diff --git a/.coveragerc b/.coveragerc index f024452c7..2f17ab1b3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,7 @@ [run] branch = True source = keystoneclient -omit = keystoneclient/tests/*,keystoneclient/openstack/* +omit = keystoneclient/tests/* [report] -ignore-errors = True +ignore_errors = True diff --git a/.gitignore b/.gitignore index 9f14a9f52..f24746a37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .coverage -.testrepository +.stestr/ subunit.log .venv *,cover @@ -21,4 +21,6 @@ doc/source/api .project .pydevproject # Temporary files created during test, but not removed -examples/pki/certs/tmp* \ No newline at end of file +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/.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/.testr.conf b/.testr.conf deleted file mode 100644 index 3d3e1e6ee..000000000 --- a/.testr.conf +++ /dev/null @@ -1,4 +0,0 @@ -[DEFAULT] -test_command=${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./keystoneclient/tests/unit} $LISTOPT $IDOPTION -test_id_option=--load-list $IDFILE -test_list_option=--list 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 index b7139eccc..604d3ac77 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,7 +1,7 @@ If you would like to contribute to the development of OpenStack, you must follow the steps documented at: - http://docs.openstack.org/infra/manual/developers.html + 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 @@ -9,7 +9,7 @@ development workflow section of this documentation to learn how changes to OpenStack should be submitted for review via the Gerrit tool: - http://docs.openstack.org/infra/manual/developers.html#development-workflow + https://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. diff --git a/HACKING.rst b/HACKING.rst index 0dfef9905..6ea94ff6f 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -2,7 +2,7 @@ Keystone Style Commandments =========================== - Step 1: Read the OpenStack Style Commandments - http://docs.openstack.org/developer/hacking/ + https://docs.openstack.org/hacking/latest/ - Step 2: Read on Exceptions @@ -17,7 +17,7 @@ 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 http://wiki.openstack.org/testr. If you'd like to learn more +be found at https://wiki.openstack.org/testr. If you'd like to learn more in depth: https://testtools.readthedocs.org/ diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 29c067650..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -include README.rst -include AUTHORS HACKING LICENSE -include ChangeLog -include run_tests.sh tox.ini -recursive-include doc * -recursive-include tests * -recursive-include tools * diff --git a/README.rst b/README.rst index 72d85ee54..6b27711a8 100644 --- a/README.rst +++ b/README.rst @@ -1,18 +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 -`developer guide `_. The master -repository is in `Git `_. +.. Change things from this point on -This code is a fork of Rackspace's python-novaclient which is in turn a fork of -`Jacobian's python-cloudservers -`_. ``python-keystoneclient`` -is licensed under the Apache License like the rest of OpenStack. +Python bindings to the OpenStack Identity API (Keystone) +======================================================== + +.. 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: @@ -22,181 +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 [--version] [--timeout ] - [--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 ] [--os-cache] - [--force-new-token] [--stale-duration ] - ... - - 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 - password-update Update own password - 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, supported API versions and - extensions. - 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: - --version Shows the client version and exits - --timeout Set request timeout (in seconds) - --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] - --os-cache Use the auth token cache. Defaults to env[OS_CACHE] - --force-new-token If the keyring is available and in use, 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 the keyring. - --stale-duration - 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 30 seconds. - - 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/authentication-plugins.rst b/doc/source/authentication-plugins.rst deleted file mode 100644 index 5afc0bc2e..000000000 --- a/doc/source/authentication-plugins.rst +++ /dev/null @@ -1,237 +0,0 @@ -====================== -Authentication Plugins -====================== - -Introduction -============ - -Authentication plugins provide a generic means by which to extend the -authentication mechanisms known to OpenStack clients. - -In the vast majority of cases the authentication plugins used will be those -written for use with the OpenStack Identity Service (Keystone), however this is -not the only possible case, and the mechanisms by which authentication plugins -are used and implemented should be generic enough to cover completely -customized authentication solutions. - -The subset of authentication plugins intended for use with an OpenStack -Identity server (such as Keystone) are called Identity Plugins. - - -Available Plugins -================= - -Keystoneclient ships with a number of plugins and particularly Identity -Plugins. - -V2 Identity Plugins -------------------- - -Standard V2 identity plugins are defined in the module: -:py:mod:`keystoneclient.auth.identity.v2` - -They include: - -- :py:class:`~keystoneclient.auth.identity.v2.Password`: Authenticate against - a V2 identity service using a username and password. -- :py:class:`~keystoneclient.auth.identity.v2.Token`: Authenticate against a - V2 identity service using an existing token. - -V2 identity plugins must use an auth_url that points to the root of a V2 -identity server URL, i.e.: `http://hostname:5000/v2.0`. - -V3 Identity Plugins -------------------- - -Standard V3 identity plugins are defined in the module -:py:mod:`keystoneclient.auth.identity.v3`. - -V3 Identity plugins are slightly different from their V2 counterparts as a V3 -authentication request can contain multiple authentication methods. To handle -this V3 defines a number of different -:py:class:`~keystoneclient.auth.identity.v3.AuthMethod` classes: - -- :py:class:`~keystoneclient.auth.identity.v3.PasswordMethod`: Authenticate - against a V3 identity service using a username and password. -- :py:class:`~keystoneclient.auth.identity.v3.TokenMethod`: Authenticate against - a V2 identity service using an existing token. - -The :py:class:`~keystoneclient.auth.identity.v3.AuthMethod` objects are then -passed to the :py:class:`~keystoneclient.auth.identity.v3.Auth` plugin:: - - >>> from keystoneclient import session - >>> from keystoneclient.auth.identity import v3 - >>> password = v3.PasswordMethod(username='user', - ... password='password') - >>> auth = v3.Auth(auth_url='http://my.keystone.com:5000/v3', - ... auth_methods=[password], - ... project_id='projectid') - >>> sess = session.Session(auth=auth) - -As in the majority of cases you will only want to use one -:py:class:`~keystoneclient.auth.identity.v3.AuthMethod` there are also helper -authentication plugins for the various -:py:class:`~keystoneclient.auth.identity.v3.AuthMethod` which can be used more -like the V2 plugins: - -- :py:class:`~keystoneclient.auth.identity.v3.Password`: Authenticate using - only a :py:class:`~keystoneclient.auth.identity.v3.PasswordMethod`. -- :py:class:`~keystoneclient.auth.identity.v3.Token`: Authenticate using only a - :py:class:`~keystoneclient.auth.identity.v3.TokenMethod`. - -:: - - >>> auth = v3.Password(auth_url='http://my.keystone.com:5000/v3', - ... username='username', - ... password='password', - ... project_id='projectid') - >>> sess = session.Session(auth=auth) - -This will have exactly the same effect as using the single -:py:class:`~keystoneclient.auth.identity.v3.PasswordMethod` above. - -V3 identity plugins must use an auth_url that points to the root of a V3 -identity server URL, i.e.: `http://hostname:5000/v3`. - -Version Independent Identity Plugins ------------------------------------- - -Standard version independent identity plugins are defined in the module -:py:mod:`keystoneclient.auth.identity.generic`. - -For the cases of plugins that exist under both the identity V2 and V3 APIs -there is an abstraction to allow the plugin to determine which of the V2 and V3 -APIs are supported by the server and use the most appropriate API. - -These plugins are: - -- :py:class:`~keystoneclient.auth.identity.generic.Password`: Authenticate - using a user/password against either v2 or v3 API. -- :py:class:`~keystoneclient.auth.identity.generic.Token`: Authenticate using - an existing token against either v2 or v3 API. - -These plugins work by first querying the identity server to determine available -versions and so the `auth_url` used with the plugins should point to the base -URL of the identity server to use. If the `auth_url` points to either a V2 or -V3 endpoint it will restrict the plugin to only working with that version of -the API. - -Simple Plugins --------------- - -In addition to the Identity plugins a simple plugin that will always use the -same provided token and endpoint is available. This is useful in situations -where you have an ``ADMIN_TOKEN`` or in testing when you specifically know the -endpoint you want to communicate with. - -It can be found at :py:class:`keystoneclient.auth.token_endpoint.Token`. - - -V3 OAuth 1.0a Plugins ---------------------- - -There also exists a plugin for OAuth 1.0a authentication. We provide a helper -authentication plugin at: -:py:class:`~keystoneclient.v3.contrib.oauth1.auth.OAuth`. -The plugin requires the OAuth consumer's key and secret, as well as the OAuth -access token's key and secret. For example:: - - >>> from keystoneclient.v3.contrib.oauth1 import auth - >>> from keystoneclient import session - >>> from keystoneclient.v3 import client - >>> a = auth.OAuth('http://my.keystone.com:5000/v3', - ... consumer_key=consumer_id, - ... consumer_secret=consumer_secret, - ... access_key=access_token_key, - ... access_secret=access_token_secret) - >>> s = session.Session(auth=a) - - -Loading Plugins by Name -======================= - -In auth_token middleware and for some service to service communication it is -possible to specify a plugin to load via name. The authentication options that -are available are then specific to the plugin that you specified. Currently the -authentication plugins that are available in `keystoneclient` are: - -- password: :py:class:`keystoneclient.auth.identity.generic.Password` -- token: :py:class:`keystoneclient.auth.identity.generic.Token` -- v2password: :py:class:`keystoneclient.auth.identity.v2.Password` -- v2token: :py:class:`keystoneclient.auth.identity.v2.Token` -- v3password: :py:class:`keystoneclient.auth.identity.v3.Password` -- v3token: :py:class:`keystoneclient.auth.identity.v3.Token` - - -Creating Authentication Plugins -=============================== - -Creating an Identity Plugin ---------------------------- - -If you have implemented a new authentication mechanism into the Identity -service then you will be able to reuse a lot of the infrastructure available -for the existing Identity mechanisms. As the V2 identity API is essentially -frozen, it is expected that new plugins are for the V3 API. - -To implement a new V3 plugin that can be combined with others you should -implement the base :py:class:`keystoneclient.auth.identity.v3.AuthMethod` class -and implement the -:py:meth:`~keystoneclient.auth.identity.v3.AuthMethod.get_auth_data` function. -If your Plugin cannot be used in conjunction with existing -:py:class:`keystoneclient.auth.identity.v3.AuthMethod` then you should just -override :py:class:`keystoneclient.auth.identity.v3.Auth` directly. - -The new :py:class:`~keystoneclient.auth.identity.v3.AuthMethod` should take all -the required parameters via -:py:meth:`~keystoneclient.auth.identity.v3.AuthMethod.__init__` and return from -:py:meth:`~keystoneclient.auth.identity.v3.AuthMethod.get_auth_data` a tuple -with the unique identifier of this plugin (e.g. *password*) and a dictionary -containing the payload of values to send to the authentication server. The -session, calling auth object and request headers are also passed to this -function so that the plugin may use or manipulate them. - -You should also provide a class that inherits from -:py:class:`keystoneclient.auth.identity.v3.Auth` with an instance of your new -:py:class:`~keystoneclient.auth.identity.v3.AuthMethod` as the `auth_methods` -parameter to :py:class:`keystoneclient.auth.identity.v3.Auth`. - -By convention (and like above) these are named `PluginType` and -`PluginTypeMethod` (for example -:py:class:`~keystoneclient.auth.identity.v3.Password` and -:py:class:`~keystoneclient.auth.identity.v3.PasswordMethod`). - - -Creating a Custom Plugin ------------------------- - -To implement an entirely new plugin you should implement the base class -:py:class:`keystoneclient.auth.base.BaseAuthPlugin` and provide the -:py:meth:`~keystoneclient.auth.base.BaseAuthPlugin.get_endpoint`, -:py:meth:`~keystoneclient.auth.base.BaseAuthPlugin.get_token` and -:py:meth:`~keystoneclient.auth.base.BaseAuthPlugin.invalidate` functions. - -:py:meth:`~keystoneclient.auth.base.BaseAuthPlugin.get_token` is called to -retrieve the string token from a plugin. It is intended that a plugin will -cache a received token and so if the token is still valid then it should be -re-used rather than fetching a new one. A session object is provided with which -the plugin can contact it's server. (Note: use `authenticated=False` when -making those requests or it will end up being called recursively). The return -value should be the token as a string. - -:py:meth:`~keystoneclient.auth.base.BaseAuthPlugin.get_endpoint` is called to -determine a base URL for a particular service's requests. The keyword arguments -provided to the function are those that are given by the `endpoint_filter` -variable in :py:meth:`keystoneclient.session.Session.request`. A session object -is also provided so that the plugin may contact an external source to determine -the endpoint. Again this will be generally be called once per request and so -it is up to the plugin to cache these responses if appropriate. The return -value should be the base URL to communicate with. - -:py:meth:`~keystoneclient.auth.base.BaseAuthPlugin.invalidate` should also be -implemented to clear the current user credentials so that on the next -:py:meth:`~keystoneclient.auth.base.BaseAuthPlugin.get_token` call a new token -can be retrieved. - -The most simple example of a plugin is the -:py:class:`keystoneclient.auth.token_endpoint.Token` plugin. diff --git a/doc/source/conf.py b/doc/source/conf.py index 4e238aa2f..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. # @@ -12,13 +10,9 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from __future__ import unicode_literals - import os import sys -import pbr.version - sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) @@ -33,11 +27,11 @@ # 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', - 'oslosphinx', + 'openstackdocstheme', ] todo_include_todos = True @@ -55,18 +49,8 @@ master_doc = 'index' # General information about the project. -project = 'python-keystoneclient' copyright = 'OpenStack Contributors' -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -version_info = pbr.version.VersionInfo('python-keystoneclient') -# The short X.Y version. -version = version_info.version_string() -# The full version, including alpha/beta/rc tags. -release = version_info.release_string() - # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None @@ -100,18 +84,15 @@ #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 = [ - ('man/keystone', 'keystone', 'Client for OpenStack Identity API', - ['OpenStack Contributors'], 1), -] +#man_pages = [] # -- Options for HTML output -------------------------------------------------- @@ -119,6 +100,7 @@ # Sphinx are currently 'default' and 'sphinxdoc'. #html_theme_path = ["."] #html_theme = '_theme' +html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -149,11 +131,6 @@ # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['static'] -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -git_cmd = "git log --pretty=format:'%ad, commit %h' --date=local -n1" -html_last_updated_fmt = os.popen(git_cmd).read() - # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True @@ -188,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 ------------------------------------------------- @@ -201,10 +186,9 @@ # (source start file, target name, title, author, documentclass [howto/manual]) # . latex_documents = [ - ('index', 'python-keystoneclient.tex', + ('index', 'doc-python-keystoneclient.tex', 'python-keystoneclient Documentation', - 'Nebula Inc, based on work by Rackspace and Jacob Kaplan-Moss', - 'manual'), + 'OpenStack', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -224,7 +208,27 @@ # If false, no module index is generated. #latex_use_modindex = True +# 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': ('http://docs.python.org/', None), - 'osloconfig': ('http://docs.openstack.org/developer/oslo.config/', None), + '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 9d931cab1..f1114b67c 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,20 +1,18 @@ +======================================================== Python bindings to the OpenStack Identity API (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), and a command-line script (installed as -:doc:`keystone `). +:mod:`keystoneclient` modules). Contents: .. toctree:: :maxdepth: 1 - man/keystone using-api-v3 using-sessions - authentication-plugins using-api-v2 api/modules @@ -24,21 +22,29 @@ 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`: http://docs.openstack.org/developer/keystone/ -.. _`WSGI Middleware`: http://docs.openstack.org/developer/keystonemiddleware/ +.. _`Identity Service`: https://docs.openstack.org/keystone/latest/ +.. _`WSGI Middleware`: https://docs.openstack.org/keystonemiddleware/latest/ + +Release Notes +============= + + +Read also the `Keystoneclient Release Notes +`_. + 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://docs.openstack.org/infra/manual/developers.html#development-workflow +.. _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/man/keystone.rst b/doc/source/man/keystone.rst deleted file mode 100644 index 10d9d93cb..000000000 --- a/doc/source/man/keystone.rst +++ /dev/null @@ -1,158 +0,0 @@ -============================================================== -:program:`keystone` command line utility (pending deprecation) -============================================================== - -.. program:: keystone -.. highlight:: bash - -SYNOPSIS -======== - -:program:`keystone` [options] [command-options] - -:program:`keystone help` - -:program:`keystone help` - - -DESCRIPTION -=========== - -.. WARNING:: - - The :program:`keystone` command line utility is pending deprecation. The - `OpenStackClient unified command line utility - `_ should be - used instead. The :program:`keystone` command line utility only supports V2 - of the Identity API whereas the OSC program supports both V2 and V3. - -The :program:`keystone` command line utility interacts with services providing -OpenStack Identity API (e.g. Keystone). - -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 projects which are -historically called tenants. To specify the project 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 project. - -.. 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 - - -OPTIONS -======= - -To get a list of available commands and options run:: - - keystone help - -To get usage and options of a command:: - - keystone help - - -EXAMPLES -======== - -Get information about endpoint-create command:: - - keystone help endpoint-create - -View endpoints of OpenStack services:: - - keystone catalog - -Create a 'service' project:: - - keystone tenant-create --name=service - -Create service user for nova:: - - keystone user-create --name=nova \ - --tenant_id= \ - --email=nova@nothing.com - -View roles:: - - keystone role-list - - -BUGS -==== - -Keystone client is hosted in Launchpad so you can view current bugs at -https://bugs.launchpad.net/python-keystoneclient/. diff --git a/doc/source/using-api-v2.rst b/doc/source/using-api-v2.rst index 6285d2143..7b0815f49 100644 --- a/doc/source/using-api-v2.rst +++ b/doc/source/using-api-v2.rst @@ -26,8 +26,8 @@ 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 @@ -45,22 +45,30 @@ 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='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 ================ @@ -71,7 +79,7 @@ This example will create a tenant named *openstackDemo*:: >>> keystone = client.Client(...) >>> keystone.tenants.create(tenant_name="openstackDemo", ... description="Default Tenant", enabled=True) - + Creating users ============== diff --git a/doc/source/using-api-v3.rst b/doc/source/using-api-v3.rst index 61b2b9d89..4f305e81f 100644 --- a/doc/source/using-api-v3.rst +++ b/doc/source/using-api-v3.rst @@ -8,6 +8,7 @@ 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` @@ -78,18 +79,17 @@ 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`` (see -:py:class:`keystoneclient.openstack.common.apiclient.exceptions.ClientException`) +:class:`keystoneclient.exceptions.ClientException`. Authenticating Using Sessions ============================= Instantiate a :py:class:`keystoneclient.v3.client.Client` using a -:py:class:`~keystoneclient.session.Session` to provide the authentication +:py:class:`~keystoneauth1.session.Session` to provide the authentication plugin, SSL/TLS certificates, and other data:: - >>> from keystoneclient.auth.identity import v3 - >>> from keystoneclient import session + >>> from keystoneauth1.identity import v3 + >>> from keystoneauth1 import session >>> from keystoneclient.v3 import client >>> auth = v3.Password(auth_url='https://my.keystone.com:5000/v3', ... user_id='myuserid', @@ -102,6 +102,31 @@ 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) ======================================= @@ -118,7 +143,7 @@ password:: ... username=username, password=password, ... user_domain_name=user_domain_name) -A :py:class:`~keystoneclient.session.Session` should be passed to the Client +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. diff --git a/doc/source/using-sessions.rst b/doc/source/using-sessions.rst index 016e7a299..a5a5a58da 100644 --- a/doc/source/using-sessions.rst +++ b/doc/source/using-sessions.rst @@ -5,7 +5,7 @@ Using Sessions Introduction ============ -The :py:class:`keystoneclient.session.Session` class was introduced into +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. @@ -55,11 +55,11 @@ service and fetch a new one. An example from keystoneclient:: - >>> from keystoneclient.auth.identity import v3 - >>> from keystoneclient import session + >>> from keystoneauth1.identity import v3 + >>> from keystoneauth1 import session >>> from keystoneclient.v3 import client - >>> auth = v3.Password(auth_url='https://my.keystone.com:5000/v3, + >>> auth = v3.Password(auth_url='https://my.keystone.com:5000/v3', ... username='myuser', ... password='mypassword', ... project_id='proj', @@ -189,11 +189,12 @@ 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:`keystoneclient.auth.token_endpoint.Token` plugin (which is used when -you want to always use a specific endpoint and token combination) will always -return the same endpoint regardless of the parameters to ``endpoint_filter`` or -a custom OpenStack authentication mechanism may not have the concept of -multiple ``interface`` options and choose to ignore that parameter. +: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 952bdaea3..6519671a5 100644 --- a/examples/pki/certs/cacert.pem +++ b/examples/pki/certs/cacert.pem @@ -1,23 +1,23 @@ -----BEGIN CERTIFICATE----- -MIID1jCCAr6gAwIBAgIJAJOtRP2+wrM/MA0GCSqGSIb3DQEBBQUAMIGeMQowCAYD -VQQFEwE1MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1bm55 -dmFsZTESMBAGA1UEChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTElMCMG -CSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxMLU2Vs -ZiBTaWduZWQwIBcNMTMwOTEzMTYyNTQyWhgPMjA3MjAzMDcxNjI1NDJaMIGeMQow -CAYDVQQFEwE1MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1 -bm55dmFsZTESMBAGA1UEChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTEl -MCMGCSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxML -U2VsZiBTaWduZWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCl8906 -EaRpibQFcCBWfxzLi5x/XpZ9iL6UX92NrSJxcDbaGws7s+GtjgDy8UOEonesRWTe -qQEZtHpC3/UHHOnsA8F6ha/pq9LioqT7RehCnZCLBJwh5Ct+lclpWs15SkjJD2LT -Dkjox0eA9nOBx+XDlWyU/GAyqx5Wsvg/Kxr0iod9/4IcJdnSdUjq4v0Cxg/zNk08 -XPJX+F0bUDhgdUf7JrAmmS5LA8wphRnbIgtVsf6VN9HrbqtHAJDxh8gEfuwdhEW1 -df1fBtZ+6WMIF3IRSbIsZELFB6sqcyRj7HhMoWMkdEyPb2f8mq61MzTgE6lJGIyT -RvEoFie7qtGADIofAgMBAAGjEzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN -AQEFBQADggEBAJRMdEwAdN+crqI9dBLYlbBbnQ8xr9mk+REMdz9+SKhDCNdVisWU -iLEZvK/aozrsRsDi81JjS4Tz0wXo8zsPPoDnXgDYEicNPTKifbPKgHdDIGFOwBKn -y2cF6fHEn8n3KIBrDCNY6rHcYGZ7lbq/8eF0GoYQboPiuYesvVpynPmIK5/Mmire -EuuZALAe1IFqqFt+l6tiJU2JWUFjLkFARMOD14qFZm+SInl64toi08j6gdou+NMW -7GEMbVHwNTafM/TgFN5j0yP9SAnYubckLSyH6hwR+rM8dztP5769joxQfnc9O/Bn -TBD9KFpeQv6VJWLAxiIKcQCRTTDJLZZ0MQI= +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 7d593efd7..000000000 --- a/examples/pki/certs/middleware.pem +++ /dev/null @@ -1,50 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDpjCCAo4CARAwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV -BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK -EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr -ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x -MzA5MTMxNjI1NDNaGA8yMDcyMDMwNzE2MjU0M1owgZAxCzAJBgNVBAYTAlVTMQsw -CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh -Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv -cGVuc3RhY2sub3JnMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDL06AaJROwHPgJ9tcySSBepzJ81jYars2sMvLjyuvd -iIBbhWvbS/a9Tw3WgL8H6OALkHiOU/f0A6Rpv8dGDIDsxZQVjT/4SLaQUOeDM+9b -fkKHpSd9G3CsdSSZgOH08n+MyZ7slPHfUHLYWso0SJD0vAi1gmGDlSM/mmhhHTpC -DGo6Wbwqare6JNeTCGJTJYwrxtoMCh/W1ZrslPC5lFvlHD7KBBf6IU2A8Xh/dUa3 -p5pmQeHPW8Em90DzIB1qH0DRXl3KANc24xYRR45pPCVkk6vFsy6P0JwwpnkszB+L -cK6CEsJhLsOYvQFsiQfSZ8m7YGhgrMLxtop4YEPirGGrAgMBAAEwDQYJKoZIhvcN -AQEFBQADggEBAAjU7YomUx/U56p1KWHvr1B7oczHF8fPHYbuk5c/N81WOJeSRy+P -5ZGZ2UPjvqqXByv+78YWMKGY1BZ/2doeWuydr0sdSxEwmIUBYxFpujuYY+0AjS/n -mMr1ZijK7TJssteKM7/MClzghUhPweDZrAg3ff1hbhK5QSy+9UPxUqLH44tfYSVC -/BzM6se0p5ToM0bwdsa8TofaBRE1L1IW/Hg4VIGOoKs0R0uLm7+Oot2me2cEuZ6h -Wls6MED8ND1Nz8EAKwndkeDu2iMM+qx/YFp6K8BQ5E5nXd2rbUZUlQMp1WbUlZ87 -KvC98aT0UYIq6uo1Lx/dQvJs7faAkYd4lmE= ------END CERTIFICATE----- ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDL06AaJROwHPgJ -9tcySSBepzJ81jYars2sMvLjyuvdiIBbhWvbS/a9Tw3WgL8H6OALkHiOU/f0A6Rp -v8dGDIDsxZQVjT/4SLaQUOeDM+9bfkKHpSd9G3CsdSSZgOH08n+MyZ7slPHfUHLY -Wso0SJD0vAi1gmGDlSM/mmhhHTpCDGo6Wbwqare6JNeTCGJTJYwrxtoMCh/W1Zrs -lPC5lFvlHD7KBBf6IU2A8Xh/dUa3p5pmQeHPW8Em90DzIB1qH0DRXl3KANc24xYR -R45pPCVkk6vFsy6P0JwwpnkszB+LcK6CEsJhLsOYvQFsiQfSZ8m7YGhgrMLxtop4 -YEPirGGrAgMBAAECggEATwvbY0hNwlb5uqOIAXBqpUqiQdexU9fG26lGmSDxKBDv -9o5frcRgBDrMWwvDCgY+HT4CAvB9kJx4/qnpVjkzJp/ZNiJ5VIiehIlbv348rXbh -xkk+bz5dDATCFOXuu1fwL2FhyM5anwhMAav0DyK1VLQ3jGzr9GO6L8hqAn+bQFFu -6ngiODwfhBMl5aRoL9UOBEhccK07znrH0JGRz+3+5Cdz59Xw91Bv210LhNNDL58+ -0JD0N+YztVOQd2bgwo0bQbOEijzmYq+0mjoqAnJh1/++y7PlIPs0AnPgqSnFPx9+ -6FsQEVRgk5Uq3kvPLaP4nT2y6MDZSp+ujYldvJhyQQKBgQDuX2pZIJMZ4aFnkG+K -TmJ5wsLa/u9an0TmvAL9RLtBpVpQNKD8cQ+y8PUZavXDbAIt5NWqZVnTbCR79Dnd -mZKblwcHhtsyA5f89el5KcxY2BREWdHdTnJpNd7XRlUECmzvX1zGj77lA982PhII -yflRBRV3vqLkgC8vfoYgRyRElwKBgQDa5jnLdx/RahfYMOgn1HE5o4hMzLR4Y0Dd -+gELshcUbPqouoP5zOb8WOagVJIgZVOSN+/VqbilVYrqRiNTn2rnoxs+HHRdaJNN -3eXllD4J2HfC2BIj1xSpIdyh2XewAJqw9IToHNB29QUhxOtgwseHciPG6JaKH2ik -kqGKH/EKDQKBgFFAftygiOPCkCTgC9UmANUmOQsy6N2H+pF3tsEj43xt44oBVnqW -A1boYXNnjRwuvdNs9BPf9i1l6E3EItFRXrLgWQoMwryakv0ryYh+YeRKyyW9RBbe -fYs1TJ8unx4Ae79gTxxztQsVNcmkgLs0NWKTjAzEE3w14V+cDhYEie1DAoGBAJdI -V5cLrBzBstsB6eBlDR9lqrRRIUS2a8U9m+1mVlcSfiWQSdehSd4K3tDdwePLw3ch -W4qR8n+pYAlLEe0gFvUhn5lMdwt7U5qUCeehjUKmrRYm2FqWsbu2IFJnBjXIJSC4 -zQXRrC0aZ0KQYpAL7XPpaVp1slyhGmPqxuO78Y0dAoGBAMHo3EIMwu9rfuGwFodr -GFsOZhfJqgo5GDNxxf89Q9WWpMDTCdX+wdBTrN/wsMbBuwIDHrUuRnk6D5CWRjSk -/ikCgHN3kOtrbL8zzqRomGAIIWKYGFEIGe1GHVGo5r//HXHdPxFXygvruQ/xbOA4 -RGvmDiji8vVDq7Shho8I6KuT ------END PRIVATE KEY----- diff --git a/examples/pki/certs/signing_cert.pem b/examples/pki/certs/signing_cert.pem index 63ab2478d..6428be8d0 100644 --- a/examples/pki/certs/signing_cert.pem +++ b/examples/pki/certs/signing_cert.pem @@ -1,22 +1,22 @@ -----BEGIN CERTIFICATE----- -MIIDpTCCAo0CAREwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV -BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK -EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr -ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x -MzA5MTMxNjI1NDNaGA8yMDcyMDMwNzE2MjU0M1owgY8xCzAJBgNVBAYTAlVTMQsw -CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh -Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv -cGVuc3RhY2sub3JnMREwDwYDVQQDEwhLZXlzdG9uZTCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBAMz5WsgsuX3rZUdLwQpZXN2Ro7LQ6jEZnreBqMztVObw -BuC1WdiJsg6dVlC7PVdt+0gY1c8WFg1TKmsucxesQSyfGAPg+9T/hsRMb6y12uJx -fp3Wgqqw0U1HsXvMiaJH87MaGnt043BxzF+R9fhAcDk6Cyj5cx9J0LvZJEOzN4J4 -ZRyO6j/DZZItb3lK5W9xkuoT+mTdDZOQJnXyG818uiWfjdCkLjr1ruytRcBOo4na -Y828voT/A7I95+YCgKgbjiUWhHeTaNmMEQiGy0nGYfteC+oSsHOlxZ3b12azzHPk -83Bh2ez0Ih9vcZoe9DqvlFOXfv9q8OsYc5Yo6gPTXEsCAwEAATANBgkqhkiG9w0B -AQUFAAOCAQEAmaYE98kOQWu6DV84ZcZP/OdT8eeu3vdB247nRj+6+GYItN/Gzqt4 -HVvz7c+FVTolCcAQQ+z3XGswI9fIJ78Hb0p9CgnLprc3L7Xtk60Im59Xlf3tcurn -r/ZnSDcjRBXKiEDrSM0VrhAnc0GoSeb6aDWopec+1hWOWfBVAg9R8yJgU9sUgO3O -0gimGyrw8eubmNhckSQLJTunUTsrkcBjuSg63wAD9OqCiX6c2eoQr+0YBp2eV2/n -aOiJXWNLbeueMKSYiJNyyvM/dlON7/56cdwDTzKzgD34TImouM5VKipUwCX1ovLu -ITLzALzpqFFzc8ugV9pMgUKtDbZoPp9EEA== +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 cdd2e4c02..00d33a7bb 100644 --- a/examples/pki/certs/ssl_cert.pem +++ b/examples/pki/certs/ssl_cert.pem @@ -1,22 +1,22 @@ -----BEGIN CERTIFICATE----- -MIIDpjCCAo4CARAwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV -BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK -EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr -ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x -MzA5MTMxNjI1NDNaGA8yMDcyMDMwNzE2MjU0M1owgZAxCzAJBgNVBAYTAlVTMQsw -CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh -Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv -cGVuc3RhY2sub3JnMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDL06AaJROwHPgJ9tcySSBepzJ81jYars2sMvLjyuvd -iIBbhWvbS/a9Tw3WgL8H6OALkHiOU/f0A6Rpv8dGDIDsxZQVjT/4SLaQUOeDM+9b -fkKHpSd9G3CsdSSZgOH08n+MyZ7slPHfUHLYWso0SJD0vAi1gmGDlSM/mmhhHTpC -DGo6Wbwqare6JNeTCGJTJYwrxtoMCh/W1ZrslPC5lFvlHD7KBBf6IU2A8Xh/dUa3 -p5pmQeHPW8Em90DzIB1qH0DRXl3KANc24xYRR45pPCVkk6vFsy6P0JwwpnkszB+L -cK6CEsJhLsOYvQFsiQfSZ8m7YGhgrMLxtop4YEPirGGrAgMBAAEwDQYJKoZIhvcN -AQEFBQADggEBAAjU7YomUx/U56p1KWHvr1B7oczHF8fPHYbuk5c/N81WOJeSRy+P -5ZGZ2UPjvqqXByv+78YWMKGY1BZ/2doeWuydr0sdSxEwmIUBYxFpujuYY+0AjS/n -mMr1ZijK7TJssteKM7/MClzghUhPweDZrAg3ff1hbhK5QSy+9UPxUqLH44tfYSVC -/BzM6se0p5ToM0bwdsa8TofaBRE1L1IW/Hg4VIGOoKs0R0uLm7+Oot2me2cEuZ6h -Wls6MED8ND1Nz8EAKwndkeDu2iMM+qx/YFp6K8BQ5E5nXd2rbUZUlQMp1WbUlZ87 -KvC98aT0UYIq6uo1Lx/dQvJs7faAkYd4lmE= +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.pem b/examples/pki/cms/auth_token_revoked.pem index 12e4f0ce7..b0312a9b0 100644 --- a/examples/pki/cms/auth_token_revoked.pem +++ b/examples/pki/cms/auth_token_revoked.pem @@ -1,79 +1,79 @@ -----BEGIN CMS----- -MIIOTQYJKoZIhvcNAQcCoIIOPjCCDjoCAQExCTAHBgUrDgMCGjCCDFoGCSqGSIb3 -DQEHAaCCDEsEggxHew0KICAgICJhY2Nlc3MiOiB7DQogICAgICAgICJ0b2tlbiI6 -IHsNCiAgICAgICAgICAgICJleHBpcmVzIjogIjIwMzgtMDEtMThUMjE6MTQ6MDda -IiwNCiAgICAgICAgICAgICJpc3N1ZWRfYXQiOiAiMjAwMi0wMS0xOFQyMToxNDow -N1oiLA0KICAgICAgICAgICAgImlkIjogInBsYWNlaG9sZGVyIiwNCiAgICAgICAg -ICAgICJ0ZW5hbnQiOiB7DQogICAgICAgICAgICAgICAgImlkIjogInRlbmFudF9p -ZDEiLA0KICAgICAgICAgICAgICAgICJlbmFibGVkIjogdHJ1ZSwNCiAgICAgICAg -ICAgICAgICAiZGVzY3JpcHRpb24iOiBudWxsLA0KICAgICAgICAgICAgICAgICJu -YW1lIjogInRlbmFudF9uYW1lMSINCiAgICAgICAgICAgIH0NCiAgICAgICAgfSwN -CiAgICAgICAgInNlcnZpY2VDYXRhbG9nIjogWw0KICAgICAgICAgICAgew0KICAg -ICAgICAgICAgICAgICJlbmRwb2ludHNfbGlua3MiOiBbXSwNCiAgICAgICAgICAg -ICAgICAiZW5kcG9pbnRzIjogWw0KICAgICAgICAgICAgICAgICAgICB7DQogICAg -ICAgICAgICAgICAgICAgICAgICAiYWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAu -MTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwNCiAg -ICAgICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAicmVnaW9uT25lIiwNCiAg -ICAgICAgICAgICAgICAgICAgICAgICJpbnRlcm5hbFVSTCI6ICJodHRwOi8vMTI3 -LjAuMC4xOjg3NzYvdjEvNjRiNmYzZmJjYzUzNDM1ZThhNjBmY2Y4OWJiNjYxN2Ei -LA0KICAgICAgICAgICAgICAgICAgICAgICAgInB1YmxpY1VSTCI6ICJodHRwOi8v -MTI3LjAuMC4xOjg3NzYvdjEvNjRiNmYzZmJjYzUzNDM1ZThhNjBmY2Y4OWJiNjYx -N2EiDQogICAgICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgICAgICBdLA0K -ICAgICAgICAgICAgICAgICJ0eXBlIjogInZvbHVtZSIsDQogICAgICAgICAgICAg -ICAgIm5hbWUiOiAidm9sdW1lIg0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAg -IHsNCiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzX2xpbmtzIjogW10sDQogICAg -ICAgICAgICAgICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAgICAgICAgICAg -ew0KICAgICAgICAgICAgICAgICAgICAgICAgImFkbWluVVJMIjogImh0dHA6Ly8x -MjcuMC4wLjE6OTI5Mi92MSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVn -aW9uIjogInJlZ2lvbk9uZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAiaW50 -ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5MjkyL3YxIiwNCiAgICAgICAg -ICAgICAgICAgICAgICAgICJwdWJsaWNVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5 -MjkyL3YxIg0KICAgICAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICAgICAg -XSwNCiAgICAgICAgICAgICAgICAidHlwZSI6ICJpbWFnZSIsDQogICAgICAgICAg -ICAgICAgIm5hbWUiOiAiZ2xhbmNlIg0KICAgICAgICAgICAgfSwNCiAgICAgICAg -ICAgIHsNCiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzX2xpbmtzIjogW10sDQog -ICAgICAgICAgICAgICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAgICAgICAg -ICAgew0KICAgICAgICAgICAgICAgICAgICAgICAgImFkbWluVVJMIjogImh0dHA6 -Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODli -YjY2MTdhIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAicmVn -aW9uT25lIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJpbnRlcm5hbFVSTCI6 -ICJodHRwOi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2 -MGZjZjg5YmI2NjE3YSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicHVibGlj -VVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQz -NWU4YTYwZmNmODliYjY2MTdhIg0KICAgICAgICAgICAgICAgICAgICB9DQogICAg -ICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAidHlwZSI6ICJjb21wdXRl -IiwNCiAgICAgICAgICAgICAgICAibmFtZSI6ICJub3ZhIg0KICAgICAgICAgICAg -fSwNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzX2xp -bmtzIjogW10sDQogICAgICAgICAgICAgICAgImVuZHBvaW50cyI6IFsNCiAgICAg -ICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICAgICAgICAgImFkbWlu -VVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6MzUzNTcvdjIuMCIsDQogICAgICAgICAg -ICAgICAgICAgICAgICAicmVnaW9uIjogIlJlZ2lvbk9uZSIsDQogICAgICAgICAg -ICAgICAgICAgICAgICAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMToz -NTM1Ny92Mi4wIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJwdWJsaWNVUkwi -OiAiaHR0cDovLzEyNy4wLjAuMTo1MDAwL3YyLjAiDQogICAgICAgICAgICAgICAg -ICAgIH0NCiAgICAgICAgICAgICAgICBdLA0KICAgICAgICAgICAgICAgICJ0eXBl -IjogImlkZW50aXR5IiwNCiAgICAgICAgICAgICAgICAibmFtZSI6ICJrZXlzdG9u -ZSINCiAgICAgICAgICAgIH0NCiAgICAgICAgXSwNCiAgICAgICAgInVzZXIiOiB7 -DQogICAgICAgICAgICAidXNlcm5hbWUiOiAicmV2b2tlZF91c2VybmFtZTEiLA0K -ICAgICAgICAgICAgInJvbGVzX2xpbmtzIjogWw0KICAgICAgICAgICAgICAgICJy -b2xlMSIsDQogICAgICAgICAgICAgICAgInJvbGUyIg0KICAgICAgICAgICAgXSwN -CiAgICAgICAgICAgICJpZCI6ICJyZXZva2VkX3VzZXJfaWQxIiwNCiAgICAgICAg -ICAgICJyb2xlcyI6IFsNCiAgICAgICAgICAgICAgICB7DQogICAgICAgICAgICAg -ICAgICAgICJpZCI6ICJmMDNmZGE4ZjhhMzI0OWIyYTcwZmIxZjE3NmE3YjYzMSIs -DQogICAgICAgICAgICAgICAgICAgICJuYW1lIjogInJvbGUxIg0KICAgICAgICAg -ICAgICAgIH0sDQogICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICAg -ICAiaWQiOiAiZjAzZmRhOGY4YTMyNDliMmE3MGZiMWYxNzZhN2I2MzEiLA0KICAg -ICAgICAgICAgICAgICAgICAibmFtZSI6ICJyb2xlMiINCiAgICAgICAgICAgICAg -ICB9DQogICAgICAgICAgICBdLA0KICAgICAgICAgICAgIm5hbWUiOiAicmV2b2tl -ZF91c2VybmFtZTEiDQogICAgICAgIH0NCiAgICB9DQp9DQoxggHKMIIBxgIBATCB -pDCBnjEKMAgGA1UEBRMBNTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRIwEAYD -VQQHEwlTdW5ueXZhbGUxEjAQBgNVBAoTCU9wZW5TdGFjazERMA8GA1UECxMIS2V5 -c3RvbmUxJTAjBgkqhkiG9w0BCQEWFmtleXN0b25lQG9wZW5zdGFjay5vcmcxFDAS -BgNVBAMTC1NlbGYgU2lnbmVkAgERMAcGBSsOAwIaMA0GCSqGSIb3DQEBAQUABIIB -AA2C5qslA4D7vzbiPJ+PzI6CWKH4fxy2nl6wFneHRlzflRGVtbk7/gwVpgHvVH8+ -FvQEWeXiCvpXDcHUae0YsdB6aifDRkRctoBwWZkSIkLtdLjZTBrwoOBD2cWPTlr6 -gFPp0ARCKVP87YXiKHXStvivZDQFbnBrPTZbGwsCZFXzDYtVPkDvgWOIzHP+olB0 -k0wrFXdTQrr62GmkUdgmY31SBLAmPRlvbFBsdM8R62EVc9Mdk7A8Xenpib6+3hPV -7Jgj5IcC3WWtI1A/WOzuEepfW5AU3bcmsJ4UrsJdZLPYqxy/FS37s7oekBOfSR+Y -WSVmaaTY21X3kOqAQULJTDI= +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_scoped.pem b/examples/pki/cms/auth_token_scoped.pem index 8754a959c..2974358ba 100644 --- a/examples/pki/cms/auth_token_scoped.pem +++ b/examples/pki/cms/auth_token_scoped.pem @@ -1,78 +1,79 @@ -----BEGIN CMS----- -MIIONwYJKoZIhvcNAQcCoIIOKDCCDiQCAQExCTAHBgUrDgMCGjCCDEQGCSqGSIb3 -DQEHAaCCDDUEggwxew0KICAgICJhY2Nlc3MiOiB7DQogICAgICAgICJ0b2tlbiI6 -IHsNCiAgICAgICAgICAgICJleHBpcmVzIjogIjIwMzgtMDEtMThUMjE6MTQ6MDda -IiwNCiAgICAgICAgICAgICJpc3N1ZWRfYXQiOiAiMjAwMi0wMS0xOFQyMToxNDow -N1oiLA0KICAgICAgICAgICAgImlkIjogInBsYWNlaG9sZGVyIiwNCiAgICAgICAg -ICAgICJ0ZW5hbnQiOiB7DQogICAgICAgICAgICAgICAgImlkIjogInRlbmFudF9p -ZDEiLA0KICAgICAgICAgICAgICAgICJlbmFibGVkIjogdHJ1ZSwNCiAgICAgICAg -ICAgICAgICAiZGVzY3JpcHRpb24iOiBudWxsLA0KICAgICAgICAgICAgICAgICJu -YW1lIjogInRlbmFudF9uYW1lMSINCiAgICAgICAgICAgIH0NCiAgICAgICAgfSwN -CiAgICAgICAgInNlcnZpY2VDYXRhbG9nIjogWw0KICAgICAgICAgICAgew0KICAg -ICAgICAgICAgICAgICJlbmRwb2ludHNfbGlua3MiOiBbXSwNCiAgICAgICAgICAg -ICAgICAiZW5kcG9pbnRzIjogWw0KICAgICAgICAgICAgICAgICAgICB7DQogICAg -ICAgICAgICAgICAgICAgICAgICAiYWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAu -MTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwNCiAg -ICAgICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAicmVnaW9uT25lIiwNCiAg -ICAgICAgICAgICAgICAgICAgICAgICJpbnRlcm5hbFVSTCI6ICJodHRwOi8vMTI3 -LjAuMC4xOjg3NzYvdjEvNjRiNmYzZmJjYzUzNDM1ZThhNjBmY2Y4OWJiNjYxN2Ei -LA0KICAgICAgICAgICAgICAgICAgICAgICAgInB1YmxpY1VSTCI6ICJodHRwOi8v -MTI3LjAuMC4xOjg3NzYvdjEvNjRiNmYzZmJjYzUzNDM1ZThhNjBmY2Y4OWJiNjYx -N2EiDQogICAgICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgICAgICBdLA0K -ICAgICAgICAgICAgICAgICJ0eXBlIjogInZvbHVtZSIsDQogICAgICAgICAgICAg -ICAgIm5hbWUiOiAidm9sdW1lIg0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAg -IHsNCiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzX2xpbmtzIjogW10sDQogICAg -ICAgICAgICAgICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAgICAgICAgICAg -ew0KICAgICAgICAgICAgICAgICAgICAgICAgImFkbWluVVJMIjogImh0dHA6Ly8x -MjcuMC4wLjE6OTI5Mi92MSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVn -aW9uIjogInJlZ2lvbk9uZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAiaW50 -ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5MjkyL3YxIiwNCiAgICAgICAg -ICAgICAgICAgICAgICAgICJwdWJsaWNVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5 -MjkyL3YxIg0KICAgICAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICAgICAg -XSwNCiAgICAgICAgICAgICAgICAidHlwZSI6ICJpbWFnZSIsDQogICAgICAgICAg -ICAgICAgIm5hbWUiOiAiZ2xhbmNlIg0KICAgICAgICAgICAgfSwNCiAgICAgICAg -ICAgIHsNCiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzX2xpbmtzIjogW10sDQog -ICAgICAgICAgICAgICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAgICAgICAg -ICAgew0KICAgICAgICAgICAgICAgICAgICAgICAgImFkbWluVVJMIjogImh0dHA6 -Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODli -YjY2MTdhIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAicmVn -aW9uT25lIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJpbnRlcm5hbFVSTCI6 -ICJodHRwOi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2 -MGZjZjg5YmI2NjE3YSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicHVibGlj -VVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQz -NWU4YTYwZmNmODliYjY2MTdhIg0KICAgICAgICAgICAgICAgICAgICB9DQogICAg -ICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAidHlwZSI6ICJjb21wdXRl -IiwNCiAgICAgICAgICAgICAgICAibmFtZSI6ICJub3ZhIg0KICAgICAgICAgICAg -fSwNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzX2xp -bmtzIjogW10sDQogICAgICAgICAgICAgICAgImVuZHBvaW50cyI6IFsNCiAgICAg -ICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICAgICAgICAgImFkbWlu -VVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6MzUzNTcvdjIuMCIsDQogICAgICAgICAg -ICAgICAgICAgICAgICAicmVnaW9uIjogIlJlZ2lvbk9uZSIsDQogICAgICAgICAg -ICAgICAgICAgICAgICAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMToz -NTM1Ny92Mi4wIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJwdWJsaWNVUkwi -OiAiaHR0cDovLzEyNy4wLjAuMTo1MDAwL3YyLjAiDQogICAgICAgICAgICAgICAg -ICAgIH0NCiAgICAgICAgICAgICAgICBdLA0KICAgICAgICAgICAgICAgICJ0eXBl -IjogImlkZW50aXR5IiwNCiAgICAgICAgICAgICAgICAibmFtZSI6ICJrZXlzdG9u -ZSINCiAgICAgICAgICAgIH0NCiAgICAgICAgXSwNCiAgICAgICAgInVzZXIiOiB7 -DQogICAgICAgICAgICAidXNlcm5hbWUiOiAidXNlcl9uYW1lMSIsDQogICAgICAg -ICAgICAicm9sZXNfbGlua3MiOiBbDQogICAgICAgICAgICAgICAgInJvbGUxIiwN -CiAgICAgICAgICAgICAgICAicm9sZTIiDQogICAgICAgICAgICBdLA0KICAgICAg -ICAgICAgImlkIjogInVzZXJfaWQxIiwNCiAgICAgICAgICAgICJyb2xlcyI6IFsN -CiAgICAgICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgICAgICJpZCI6ICJm -MDNmZGE4ZjhhMzI0OWIyYTcwZmIxZjE3NmE3YjYzMSIsDQogICAgICAgICAgICAg -ICAgICAgICJuYW1lIjogInJvbGUxIg0KICAgICAgICAgICAgICAgIH0sDQogICAg -ICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICAgICAiaWQiOiAiZjAzZmRh -OGY4YTMyNDliMmE3MGZiMWYxNzZhN2I2MzEiLA0KICAgICAgICAgICAgICAgICAg -ICAibmFtZSI6ICJyb2xlMiINCiAgICAgICAgICAgICAgICB9DQogICAgICAgICAg -ICBdLA0KICAgICAgICAgICAgIm5hbWUiOiAidXNlcl9uYW1lMSINCiAgICAgICAg -fQ0KICAgIH0NCn0NCjGCAcowggHGAgEBMIGkMIGeMQowCAYDVQQFEwE1MQswCQYD -VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1bm55dmFsZTESMBAGA1UE -ChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTElMCMGCSqGSIb3DQEJARYW -a2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxMLU2VsZiBTaWduZWQCAREw -BwYFKw4DAhowDQYJKoZIhvcNAQEBBQAEggEAxyHPqb53KXaWJH1IE6IFp3zzm5vl -zlotcMxMepMRIxQPUDwJrP2ZJwXemQXVTpRa3Aer7hSkCRlyI++mcj/rD4h5Ygb0 -q9sscjfeZB11Y436E4ZhXCdTfrtmKyBlHMqyhTBz64zroN0P+DVH7OLZDX/gqN2U -KTX99HTN+LvUa8VqQYIzsjNv80CU6pog/YOCGPixjMKE9m9xYUr9huKZUxliHtX2 -AHoCfQPhI8nsnNHLzCx6u5xIM7A69ZIDPQ82hSHC58k+g0bq9uflRCixBSD7ulR7 -7ZRJM8IgOgFGpNeuyKcHJsCdPpZS8p1MmDCkwTOt5Kvf7Nopz+Cc325uOA== +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_expired.pem b/examples/pki/cms/auth_token_scoped_expired.pem index 43e09f333..9c4bdb9e1 100644 --- a/examples/pki/cms/auth_token_scoped_expired.pem +++ b/examples/pki/cms/auth_token_scoped_expired.pem @@ -1,76 +1,79 @@ -----BEGIN CMS----- -MIINuQYJKoZIhvcNAQcCoIINqjCCDaYCAQExCTAHBgUrDgMCGjCCC8YGCSqGSIb3 -DQEHAaCCC7cEgguzew0KICAgICJhY2Nlc3MiOiB7DQogICAgICAgICJ0b2tlbiI6 -IHsNCiAgICAgICAgICAgICJleHBpcmVzIjogIjIwMTAtMDYtMDJUMTQ6NDc6MzRa -IiwNCiAgICAgICAgICAgICJpc3N1ZWRfYXQiOiAiMjAwMi0wMS0xOFQyMToxNDow -N1oiLA0KICAgICAgICAgICAgImlkIjogInBsYWNlaG9sZGVyIiwNCiAgICAgICAg -ICAgICJ0ZW5hbnQiOiB7DQogICAgICAgICAgICAgICAgImlkIjogInRlbmFudF9p -ZDEiLA0KICAgICAgICAgICAgICAgICJlbmFibGVkIjogdHJ1ZSwNCiAgICAgICAg -ICAgICAgICAiZGVzY3JpcHRpb24iOiBudWxsLA0KICAgICAgICAgICAgICAgICJu -YW1lIjogInRlbmFudF9uYW1lMSINCiAgICAgICAgICAgIH0NCiAgICAgICAgfSwN -CiAgICAgICAgInNlcnZpY2VDYXRhbG9nIjogWw0KICAgICAgICAgICAgew0KICAg -ICAgICAgICAgICAgICJlbmRwb2ludHNfbGlua3MiOiBbXSwNCiAgICAgICAgICAg -ICAgICAiZW5kcG9pbnRzIjogWw0KICAgICAgICAgICAgICAgICAgICB7DQogICAg -ICAgICAgICAgICAgICAgICAgICAiYWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAu -MTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwNCiAg -ICAgICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAicmVnaW9uT25lIiwNCiAg -ICAgICAgICAgICAgICAgICAgICAgICJpbnRlcm5hbFVSTCI6ICJodHRwOi8vMTI3 -LjAuMC4xOjg3NzYvdjEvNjRiNmYzZmJjYzUzNDM1ZThhNjBmY2Y4OWJiNjYxN2Ei -LA0KICAgICAgICAgICAgICAgICAgICAgICAgInB1YmxpY1VSTCI6ICJodHRwOi8v -MTI3LjAuMC4xOjg3NzYvdjEvNjRiNmYzZmJjYzUzNDM1ZThhNjBmY2Y4OWJiNjYx -N2EiDQogICAgICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgICAgICBdLA0K -ICAgICAgICAgICAgICAgICJ0eXBlIjogInZvbHVtZSIsDQogICAgICAgICAgICAg -ICAgIm5hbWUiOiAidm9sdW1lIg0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAg -IHsNCiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzX2xpbmtzIjogW10sDQogICAg -ICAgICAgICAgICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAgICAgICAgICAg -ew0KICAgICAgICAgICAgICAgICAgICAgICAgImFkbWluVVJMIjogImh0dHA6Ly8x -MjcuMC4wLjE6OTI5Mi92MSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVn -aW9uIjogInJlZ2lvbk9uZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAiaW50 -ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5MjkyL3YxIiwNCiAgICAgICAg -ICAgICAgICAgICAgICAgICJwdWJsaWNVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5 -MjkyL3YxIg0KICAgICAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICAgICAg -XSwNCiAgICAgICAgICAgICAgICAidHlwZSI6ICJpbWFnZSIsDQogICAgICAgICAg -ICAgICAgIm5hbWUiOiAiZ2xhbmNlIg0KICAgICAgICAgICAgfSwNCiAgICAgICAg -ICAgIHsNCiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzX2xpbmtzIjogW10sDQog -ICAgICAgICAgICAgICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAgICAgICAg -ICAgew0KICAgICAgICAgICAgICAgICAgICAgICAgImFkbWluVVJMIjogImh0dHA6 -Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODli -YjY2MTdhIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAicmVn -aW9uT25lIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJpbnRlcm5hbFVSTCI6 -ICJodHRwOi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2 -MGZjZjg5YmI2NjE3YSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicHVibGlj -VVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQz -NWU4YTYwZmNmODliYjY2MTdhIg0KICAgICAgICAgICAgICAgICAgICB9DQogICAg -ICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAidHlwZSI6ICJjb21wdXRl -IiwNCiAgICAgICAgICAgICAgICAibmFtZSI6ICJub3ZhIg0KICAgICAgICAgICAg -fSwNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzX2xp -bmtzIjogW10sDQogICAgICAgICAgICAgICAgImVuZHBvaW50cyI6IFsNCiAgICAg -ICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICAgICAgICAgImFkbWlu -VVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6MzUzNTcvdjIuMCIsDQogICAgICAgICAg -ICAgICAgICAgICAgICAicmVnaW9uIjogIlJlZ2lvbk9uZSIsDQogICAgICAgICAg -ICAgICAgICAgICAgICAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMToz -NTM1Ny92Mi4wIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJwdWJsaWNVUkwi -OiAiaHR0cDovLzEyNy4wLjAuMTo1MDAwL3YyLjAiDQogICAgICAgICAgICAgICAg -ICAgIH0NCiAgICAgICAgICAgICAgICBdLA0KICAgICAgICAgICAgICAgICJ0eXBl -IjogImlkZW50aXR5IiwNCiAgICAgICAgICAgICAgICAibmFtZSI6ICJrZXlzdG9u -ZSINCiAgICAgICAgICAgIH0NCiAgICAgICAgXSwNCiAgICAgICAgInVzZXIiOiB7 -DQogICAgICAgICAgICAidXNlcm5hbWUiOiAidXNlcl9uYW1lMSIsDQogICAgICAg -ICAgICAicm9sZXNfbGlua3MiOiBbDQogICAgICAgICAgICAgICAgInJvbGUxIiwN -CiAgICAgICAgICAgICAgICAicm9sZTIiDQogICAgICAgICAgICBdLA0KICAgICAg -ICAgICAgImlkIjogInVzZXJfaWQxIiwNCiAgICAgICAgICAgICJyb2xlcyI6IFsN -CiAgICAgICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgICAgICJuYW1lIjog -InJvbGUxIg0KICAgICAgICAgICAgICAgIH0sDQogICAgICAgICAgICAgICAgew0K -ICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICJyb2xlMiINCiAgICAgICAgICAg -ICAgICB9DQogICAgICAgICAgICBdLA0KICAgICAgICAgICAgIm5hbWUiOiAidXNl -cl9uYW1lMSINCiAgICAgICAgfQ0KICAgIH0NCn0NCjGCAcowggHGAgEBMIGkMIGe -MQowCAYDVQQFEwE1MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcT -CVN1bm55dmFsZTESMBAGA1UEChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9u -ZTElMCMGCSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UE -AxMLU2VsZiBTaWduZWQCAREwBwYFKw4DAhowDQYJKoZIhvcNAQEBBQAEggEAw7K9 -7FaxXE6QNbsWmTAo/mtppDB2hv2DCwxMnjaZuOlV3g7UGnF8mxHjWd2Pcj1r0oGb -0iACE9qmoZVHTPWU6WWBClAIF/bcs6Y+5S10bCu1uRVrzUCsLEbbJOLxBZG1qiEZ -opLn6pBIOY8ovxcoKKmI56JgsqVGclZM5yH9Z9E5hSZgMREJZFZcVHA3pTJeTjc2 -9Mpb3RS5Q/FXf2nP09YA4Mp9+J15gFH/YuhBQiyo+LqvHtg+DdWdxcM3keAaTuxw -Z8Cd26T+cTv1iS5qXcykd8OP7V0eIF7i39wshXGm6B9XpwFEYiLTZy7398O/yeGd -izImJNpCowBA0Pyr8w== +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_unscoped.pem b/examples/pki/cms/auth_token_unscoped.pem index 274ac3860..bfb97968f 100644 --- a/examples/pki/cms/auth_token_unscoped.pem +++ b/examples/pki/cms/auth_token_unscoped.pem @@ -1,29 +1,29 @@ -----BEGIN CMS----- -MIIE9gYJKoZIhvcNAQcCoIIE5zCCBOMCAQExCTAHBgUrDgMCGjCCAwMGCSqGSIb3 -DQEHAaCCAvQEggLwew0KICAgICJhY2Nlc3MiOiB7DQogICAgICAgICJ0b2tlbiI6 -IHsNCiAgICAgICAgICAgICJleHBpcmVzIjogIjIxMTItMDgtMTdUMTU6MzU6MzRa -IiwNCiAgICAgICAgICAgICJpc3N1ZWRfYXQiOiAiMjAwMi0wMS0xOFQyMToxNDow -N1oiLA0KICAgICAgICAgICAgImlkIjogIjAxZTAzMmM5OTZlZjQ0MDZiMTQ0MzM1 -OTE1YTQxZTc5Ig0KICAgICAgICB9LA0KICAgICAgICAic2VydmljZUNhdGFsb2ci -OiB7fSwNCiAgICAgICAgInVzZXIiOiB7DQogICAgICAgICAgICAidXNlcm5hbWUi -OiAidXNlcl9uYW1lMSIsDQogICAgICAgICAgICAicm9sZXNfbGlua3MiOiBbXSwN -CiAgICAgICAgICAgICJpZCI6ICJjOWM4OWUzYmUzZWU0NTNmYmYwMGM3OTY2ZjZk -M2ZiZCIsDQogICAgICAgICAgICAicm9sZXMiOiBbDQogICAgICAgICAgICAgICAg -ew0KICAgICAgICAgICAgICAgICAgICAiaWQiOiAiMzU5ZGE0MmQzMWMwNDQzN2Ez -MjgxMmFlYjc5ZTljMGIiLA0KICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICJy -b2xlMSINCiAgICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgIHsNCiAg -ICAgICAgICAgICAgICAgICAgImlkIjogIjU4MWFmMTk3MjZmYTRhZjViZGE3NDU3 -ODlhYjJiZjJiIiwNCiAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAicm9sZTIi -DQogICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgXSwNCiAgICAgICAgICAg -ICJuYW1lIjogInVzZXJfbmFtZTEiDQogICAgICAgIH0NCiAgICB9DQp9DQoxggHK -MIIBxgIBATCBpDCBnjEKMAgGA1UEBRMBNTELMAkGA1UEBhMCVVMxCzAJBgNVBAgT -AkNBMRIwEAYDVQQHEwlTdW5ueXZhbGUxEjAQBgNVBAoTCU9wZW5TdGFjazERMA8G -A1UECxMIS2V5c3RvbmUxJTAjBgkqhkiG9w0BCQEWFmtleXN0b25lQG9wZW5zdGFj -ay5vcmcxFDASBgNVBAMTC1NlbGYgU2lnbmVkAgERMAcGBSsOAwIaMA0GCSqGSIb3 -DQEBAQUABIIBAMmrhRIUjSd+SLUAYn+18MDB8MXiaiF+FJQbu86IFW3OpL86ksvg -CTP44Rvu1F4vvoZAQ60/tOfFVNTnBgnMv0NEfl4huiFqYrXjCphnNFQ5OYnmU6LR -bFV+dvjZXWUn0wJDroUUEjbgyy/mqUnULzQgUzyK7Ho8T0dWahQc7EFMNVjoeKfa -K7DeRe9trNNHM8anKVaeKhpWIfzbxiwIwypukce6wVGfdhaP+58jeFnGwHUIsY8V -8rzWj9UN46ko61piMAZljcktbrpqw2fDJ1H9Xl23G83rnXY7uVLQWUe7fRcUFtQt -gQvKsGkN2hqlOgMT/FxFM3HC8kcl3wmzrNA= +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_v3_token_revoked.pem b/examples/pki/cms/auth_v3_token_revoked.pem index ca2bf06be..0b1ecbf4a 100644 --- a/examples/pki/cms/auth_v3_token_revoked.pem +++ b/examples/pki/cms/auth_v3_token_revoked.pem @@ -1,123 +1,125 @@ -----BEGIN CMS----- -MIIWqQYJKoZIhvcNAQcCoIIWmjCCFpYCAQExCTAHBgUrDgMCGjCCFLYGCSqGSIb3 -DQEHAaCCFKcEghSjew0KICAgICJ0b2tlbiI6IHsNCiAgICAgICAgImNhdGFsb2ci -OiBbDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50cyI6 -IFsNCiAgICAgICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICAgICAg -ICAgImlkIjogIjNiNWU1NTRiY2YxMTRmMjQ4M2U4YTFiZTdhMDUwNmQxIiwNCiAg -ICAgICAgICAgICAgICAgICAgICAgICJpbnRlcmZhY2UiOiAiYWRtaW4iLA0KICAg -ICAgICAgICAgICAgICAgICAgICAgInVybCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3 -NzYvdjEvNjRiNmYzZmJjYzUzNDM1ZThhNjBmY2Y4OWJiNjYxN2EiLA0KICAgICAg -ICAgICAgICAgICAgICAgICAgInJlZ2lvbiI6ICJyZWdpb25PbmUiDQogICAgICAg -ICAgICAgICAgICAgIH0sDQogICAgICAgICAgICAgICAgICAgIHsNCiAgICAgICAg -ICAgICAgICAgICAgICAgICJpZCI6ICI1NGFiZDJkYzQ2M2M0YmE0YTcyOTE1NDk4 -ZjhlY2FkMSIsDQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJmYWNlIjog -ImludGVybmFsIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJ1cmwiOiAiaHR0 -cDovLzEyNy4wLjAuMTo4Nzc2L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODli -YjY2MTdhIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAicmVn -aW9uT25lIg0KICAgICAgICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAg -ICAgICB7DQogICAgICAgICAgICAgICAgICAgICAgICAiaWQiOiAiNzBhN2VmYTRi -MWI5NDE5NjgzNTdjYzQzYWUxNDE5ZWUiLA0KICAgICAgICAgICAgICAgICAgICAg -ICAgImludGVyZmFjZSI6ICJwdWJsaWMiLA0KICAgICAgICAgICAgICAgICAgICAg -ICAgInVybCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzYvdjEvNjRiNmYzZmJjYzUz -NDM1ZThhNjBmY2Y4OWJiNjYxN2EiLA0KICAgICAgICAgICAgICAgICAgICAgICAg -InJlZ2lvbiI6ICJyZWdpb25PbmUiDQogICAgICAgICAgICAgICAgICAgIH0NCiAg -ICAgICAgICAgICAgICBdLA0KICAgICAgICAgICAgICAgICJpZCI6ICI1NzA3YzNm -YzBhMjk0NzAzYTNjNjM4ZTljZjZhNmMzYSIsDQogICAgICAgICAgICAgICAgInR5 -cGUiOiAidm9sdW1lIiwNCiAgICAgICAgICAgICAgICAibmFtZSI6ICJ2b2x1bWUi -DQogICAgICAgICAgICB9LA0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAg -ICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAgICAgICAgIHsNCiAgICAgICAg -ICAgICAgICAgICAgICAgICJpZCI6ICI5MjIxN2EzYjk1Mzk0NDkyODU5YmM0OWZk -NDc0MzgyZiIsDQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJmYWNlIjog -ImFkbWluIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJ1cmwiOiAiaHR0cDov -LzEyNy4wLjAuMTo5MjkyL3YxIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJy +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 -OiAiZjIwNTYzYmRmNjZmNGVmYThhMWYxMWQ5OWI2NzJiZTEiLA0KICAgICAgICAg +OiAiOWUwM2M0NmM4MGEzNGExNTljYjM5ZjVjYjA0OThiOTIiLA0KICAgICAgICAg ICAgICAgICAgICAgICAgImludGVyZmFjZSI6ICJpbnRlcm5hbCIsDQogICAgICAg -ICAgICAgICAgICAgICAgICAidXJsIjogImh0dHA6Ly8xMjcuMC4wLjE6OTI5Mi92 -MSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJlZ2lvbk9u -ZSINCiAgICAgICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICAgICAg -ew0KICAgICAgICAgICAgICAgICAgICAgICAgImlkIjogIjM3NWY5YmE0NTlhNDQ3 -NzM4ZmI2MGZlNWZjMjZlOWFhIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJp -bnRlcmZhY2UiOiAicHVibGljIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJ1 -cmwiOiAiaHR0cDovLzEyNy4wLjAuMTo5MjkyL3YxIiwNCiAgICAgICAgICAgICAg -ICAgICAgICAgICJyZWdpb24iOiAicmVnaW9uT25lIg0KICAgICAgICAgICAgICAg -ICAgICB9DQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAiaWQi -OiAiMTVjMjFhYWU2YjI3NGE4ZGE1MmUwYTA2OGU5MDhhYWMiLA0KICAgICAgICAg -ICAgICAgICJ0eXBlIjogImltYWdlIiwNCiAgICAgICAgICAgICAgICAibmFtZSI6 -ICJnbGFuY2UiDQogICAgICAgICAgICB9LA0KICAgICAgICAgICAgew0KICAgICAg -ICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAgICAgICAgICAgICAgICAgIHsN -CiAgICAgICAgICAgICAgICAgICAgICAgICJpZCI6ICJlZGJkOWY1MGY2Njc0NmFl -OWVkMTFkYzNiMWFlMzVkYSIsDQogICAgICAgICAgICAgICAgICAgICAgICAiaW50 -ZXJmYWNlIjogImFkbWluIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJ1cmwi -OiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc0L3YxLjEvNjRiNmYzZmJjYzUzNDM1ZThh -NjBmY2Y4OWJiNjYxN2EiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInJlZ2lv -biI6ICJyZWdpb25PbmUiDQogICAgICAgICAgICAgICAgICAgIH0sDQogICAgICAg -ICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJpZCI6ICI5 -ZTAzYzQ2YzgwYTM0YTE1OWNiMzlmNWNiMDQ5OGI5MiIsDQogICAgICAgICAgICAg -ICAgICAgICAgICAiaW50ZXJmYWNlIjogImludGVybmFsIiwNCiAgICAgICAgICAg -ICAgICAgICAgICAgICJ1cmwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc0L3YxLjEv -NjRiNmYzZmJjYzUzNDM1ZThhNjBmY2Y4OWJiNjYxN2EiLA0KICAgICAgICAgICAg -ICAgICAgICAgICAgInJlZ2lvbiI6ICJyZWdpb25PbmUiDQogICAgICAgICAgICAg -ICAgICAgIH0sDQogICAgICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAg -ICAgICAgICAgICJpZCI6ICIxZGYwYjQ0ZDkyNjM0ZDU5YmQwZTBkNjBjZjdjZTQz -MiIsDQogICAgICAgICAgICAgICAgICAgICAgICAiaW50ZXJmYWNlIjogInB1Ymxp -YyIsDQogICAgICAgICAgICAgICAgICAgICAgICAidXJsIjogImh0dHA6Ly8xMjcu -MC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdh -IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAicmVnaW9uT25l -Ig0KICAgICAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICAgICAgXSwNCiAg -ICAgICAgICAgICAgICAiaWQiOiAiMmY0MDRmZGI4OTE1NGM1ODllZmJjMTA3MjZi -MDI5ZWMiLA0KICAgICAgICAgICAgICAgICJ0eXBlIjogImNvbXB1dGUiLA0KICAg -ICAgICAgICAgICAgICJuYW1lIjogIm5vdmEiDQogICAgICAgICAgICB9LA0KICAg -ICAgICAgICAgew0KICAgICAgICAgICAgICAgICJlbmRwb2ludHMiOiBbDQogICAg -ICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJpZCI6 -ICJhNDUwMWUxNDFhNGI0ZTE0YmYyODJlN2JmZmQ4MWRjNSIsDQogICAgICAgICAg -ICAgICAgICAgICAgICAiaW50ZXJmYWNlIjogImFkbWluIiwNCiAgICAgICAgICAg -ICAgICAgICAgICAgICJ1cmwiOiAiaHR0cDovLzEyNy4wLjAuMTozNTM1Ny92MyIs -DQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogIlJlZ2lvbk9uZSIN -CiAgICAgICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICAgICAgew0K -ICAgICAgICAgICAgICAgICAgICAgICAgImlkIjogIjNkMTdlMzIyN2JmYzQ0ODNi -NThkZTVlYWE1ODRlMzYwIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJpbnRl -cmZhY2UiOiAiaW50ZXJuYWwiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInVy -bCI6ICJodHRwOi8vMTI3LjAuMC4xOjM1MzU3L3YzIiwNCiAgICAgICAgICAgICAg -ICAgICAgICAgICJyZWdpb24iOiAiUmVnaW9uT25lIg0KICAgICAgICAgICAgICAg -ICAgICB9LA0KICAgICAgICAgICAgICAgICAgICB7DQogICAgICAgICAgICAgICAg -ICAgICAgICAiaWQiOiAiOGNkNGI5NTcwOTBmNGNhNTg0MmEyMmU5YTc0MDk5Y2Qi -LA0KICAgICAgICAgICAgICAgICAgICAgICAgImludGVyZmFjZSI6ICJwdWJsaWMi -LA0KICAgICAgICAgICAgICAgICAgICAgICAgInVybCI6ICJodHRwOi8vMTI3LjAu -MC4xOjUwMDAvdjMiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInJlZ2lvbiI6 -ICJSZWdpb25PbmUiDQogICAgICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAg -ICAgICBdLA0KICAgICAgICAgICAgICAgICJpZCI6ICJjNWQ5MjZkNTY2NDI0ZTRm -YmE0ZjgwYzM3OTE2Y2RlNSIsDQogICAgICAgICAgICAgICAgInR5cGUiOiAiaWRl -bnRpdHkiLA0KICAgICAgICAgICAgICAgICJuYW1lIjogImtleXN0b25lIg0KICAg -ICAgICAgICAgfQ0KICAgICAgICBdLA0KICAgICAgICAiaXNzdWVkX2F0IjogIjIw -MDItMDEtMThUMjE6MTQ6MDdaIiwNCiAgICAgICAgImV4cGlyZXNfYXQiOiAiMjAz -OC0wMS0xOFQyMToxNDowN1oiLA0KICAgICAgICAicHJvamVjdCI6IHsNCiAgICAg -ICAgICAgICJlbmFibGVkIjogdHJ1ZSwNCiAgICAgICAgICAgICJkZXNjcmlwdGlv -biI6IG51bGwsDQogICAgICAgICAgICAibmFtZSI6ICJ0ZW5hbnRfbmFtZTEiLA0K -ICAgICAgICAgICAgImlkIjogInRlbmFudF9pZDEiLA0KICAgICAgICAgICAgImRv -bWFpbiI6IHsNCiAgICAgICAgICAgICAgICAiaWQiOiAiZG9tYWluX2lkMSIsDQog -ICAgICAgICAgICAgICAgIm5hbWUiOiAiZG9tYWluX25hbWUxIg0KICAgICAgICAg -ICAgfQ0KICAgICAgICB9LA0KICAgICAgICAidXNlciI6IHsNCiAgICAgICAgICAg -ICJuYW1lIjogInJldm9rZWRfdXNlcm5hbWUxIiwNCiAgICAgICAgICAgICJpZCI6 -ICJyZXZva2VkX3VzZXJfaWQxIiwNCiAgICAgICAgICAgICJkb21haW4iOiB7DQog -ICAgICAgICAgICAgICAgImlkIjogImRvbWFpbl9pZDEiLA0KICAgICAgICAgICAg -ICAgICJuYW1lIjogImRvbWFpbl9uYW1lMSINCiAgICAgICAgICAgIH0NCiAgICAg -ICAgfSwNCiAgICAgICAgInJvbGVzIjogWw0KICAgICAgICAgICAgew0KICAgICAg -ICAgICAgICAgICJpZCI6ICJmMDNmZGE4ZjhhMzI0OWIyYTcwZmIxZjE3NmE3YjYz -MSIsDQogICAgICAgICAgICAgICAgIm5hbWUiOiAicm9sZTEiDQogICAgICAgICAg -ICB9LA0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICJpZCI6ICJmMDNm -ZGE4ZjhhMzI0OWIyYTcwZmIxZjE3NmE3YjYzMSIsDQogICAgICAgICAgICAgICAg -Im5hbWUiOiAicm9sZTIiDQogICAgICAgICAgICB9DQogICAgICAgIF0sDQogICAg -ICAgICJtZXRob2RzIjogWw0KICAgICAgICAgICAgInBhc3N3b3JkIg0KICAgICAg -ICBdDQogICAgfQ0KfQ0KMYIByjCCAcYCAQEwgaQwgZ4xCjAIBgNVBAUTATUxCzAJ -BgNVBAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYD -VQQKEwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkB -FhZrZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZAIB -ETAHBgUrDgMCGjANBgkqhkiG9w0BAQEFAASCAQCy1xOK1+nQy8tL3fORdWkcp0Y5 -88cNgl4sXmJOE1TOOEauMyVWE188gtxHelVDCFWr8kICALvAnPX0UbIhoEaxscey -mvcUazUMP2WWSsBMgSXBfbl6amTZp5KMgpMmAuGjP1xok3yvOecEF6Szh8yE3Q5O -sNEKsMI5UiJTDU7WWSUp1Zs7E4UvFjAepZGhIQWOCxSvEnrl3Mfw1f7HWKDBlijR -4XnPqJPiTmYLjzyDmi31GOHWZM4nZxShHfidLblPV4AyA/gsCh27/cZxYW/Q+cyL -wfQogs4g7XfNgLdDHlbvv7NCS06RhydhLeiqNUcCp4hnZZC16KDPWzJ2Ql5y +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_scoped.pem b/examples/pki/cms/auth_v3_token_scoped.pem index 50641147f..ae3f4f37c 100644 --- a/examples/pki/cms/auth_v3_token_scoped.pem +++ b/examples/pki/cms/auth_v3_token_scoped.pem @@ -1,123 +1,125 @@ -----BEGIN CMS----- -MIIWmgYJKoZIhvcNAQcCoIIWizCCFocCAQExCTAHBgUrDgMCGjCCFKcGCSqGSIb3 -DQEHAaCCFJgEghSUew0KICAgICJ0b2tlbiI6IHsNCiAgICAgICAgIm1ldGhvZHMi -OiBbDQogICAgICAgICAgICAicGFzc3dvcmQiDQogICAgICAgIF0sDQogICAgICAg -ICJyb2xlcyI6IFsNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAiaWQi -OiAiZjAzZmRhOGY4YTMyNDliMmE3MGZiMWYxNzZhN2I2MzEiLA0KICAgICAgICAg -ICAgICAgICJuYW1lIjogInJvbGUxIg0KICAgICAgICAgICAgfSwNCiAgICAgICAg -ICAgIHsNCiAgICAgICAgICAgICAgICAiaWQiOiAiZjAzZmRhOGY4YTMyNDliMmE3 -MGZiMWYxNzZhN2I2MzEiLA0KICAgICAgICAgICAgICAgICJuYW1lIjogInJvbGUy -Ig0KICAgICAgICAgICAgfQ0KICAgICAgICBdLA0KICAgICAgICAiaXNzdWVkX2F0 -IjogIjIwMDItMDEtMThUMjE6MTQ6MDdaIiwNCiAgICAgICAgImV4cGlyZXNfYXQi -OiAiMjAzOC0wMS0xOFQyMToxNDowN1oiLA0KICAgICAgICAicHJvamVjdCI6IHsN -CiAgICAgICAgICAgICJpZCI6ICJ0ZW5hbnRfaWQxIiwNCiAgICAgICAgICAgICJk -b21haW4iOiB7DQogICAgICAgICAgICAgICAgImlkIjogImRvbWFpbl9pZDEiLA0K -ICAgICAgICAgICAgICAgICJuYW1lIjogImRvbWFpbl9uYW1lMSINCiAgICAgICAg -ICAgIH0sDQogICAgICAgICAgICAiZW5hYmxlZCI6IHRydWUsDQogICAgICAgICAg -ICAiZGVzY3JpcHRpb24iOiBudWxsLA0KICAgICAgICAgICAgIm5hbWUiOiAidGVu -YW50X25hbWUxIg0KICAgICAgICB9LA0KICAgICAgICAiY2F0YWxvZyI6IFsNCiAg -ICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAiZW5kcG9pbnRzIjogWw0KICAg -ICAgICAgICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgICAgICAgICAiaWQi -OiAiM2I1ZTU1NGJjZjExNGYyNDgzZThhMWJlN2EwNTA2ZDEiLA0KICAgICAgICAg -ICAgICAgICAgICAgICAgImludGVyZmFjZSI6ICJhZG1pbiIsDQogICAgICAgICAg +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 -ICAgICAgICAgImlkIjogIjU0YWJkMmRjNDYzYzRiYTRhNzI5MTU0OThmOGVjYWQx -IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJpbnRlcmZhY2UiOiAiaW50ZXJu -YWwiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInVybCI6ICJodHRwOi8vMTI3 -LjAuMC4xOjg3NzYvdjEvNjRiNmYzZmJjYzUzNDM1ZThhNjBmY2Y4OWJiNjYxN2Ei -LA0KICAgICAgICAgICAgICAgICAgICAgICAgInJlZ2lvbiI6ICJyZWdpb25PbmUi -DQogICAgICAgICAgICAgICAgICAgIH0sDQogICAgICAgICAgICAgICAgICAgIHsN -CiAgICAgICAgICAgICAgICAgICAgICAgICJpZCI6ICI3MGE3ZWZhNGIxYjk0MTk2 -ODM1N2NjNDNhZTE0MTllZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAiaW50 -ZXJmYWNlIjogInB1YmxpYyIsDQogICAgICAgICAgICAgICAgICAgICAgICAidXJs -IjogImh0dHA6Ly8xMjcuMC4wLjE6ODc3Ni92MS82NGI2ZjNmYmNjNTM0MzVlOGE2 -MGZjZjg5YmI2NjE3YSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9u -IjogInJlZ2lvbk9uZSINCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAg -ICAgICAgIF0sDQogICAgICAgICAgICAgICAgImlkIjogIjU3MDdjM2ZjMGEyOTQ3 -MDNhM2M2MzhlOWNmNmE2YzNhIiwNCiAgICAgICAgICAgICAgICAidHlwZSI6ICJ2 -b2x1bWUiLA0KICAgICAgICAgICAgICAgICJuYW1lIjogInZvbHVtZSINCiAgICAg -ICAgICAgIH0sDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgImVuZHBv -aW50cyI6IFsNCiAgICAgICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAg -ICAgICAgICAgImlkIjogIjkyMjE3YTNiOTUzOTQ0OTI4NTliYzQ5ZmQ0NzQzODJm -IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJpbnRlcmZhY2UiOiAiYWRtaW4i -LA0KICAgICAgICAgICAgICAgICAgICAgICAgInVybCI6ICJodHRwOi8vMTI3LjAu -MC4xOjkyOTIvdjEiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInJlZ2lvbiI6 -ICJyZWdpb25PbmUiDQogICAgICAgICAgICAgICAgICAgIH0sDQogICAgICAgICAg -ICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJpZCI6ICJmMjA1 -NjNiZGY2NmY0ZWZhOGExZjExZDk5YjY3MmJlMSIsDQogICAgICAgICAgICAgICAg -ICAgICAgICAiaW50ZXJmYWNlIjogImludGVybmFsIiwNCiAgICAgICAgICAgICAg -ICAgICAgICAgICJ1cmwiOiAiaHR0cDovLzEyNy4wLjAuMTo5MjkyL3YxIiwNCiAg -ICAgICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAicmVnaW9uT25lIg0KICAg -ICAgICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgICAgICB7DQogICAg -ICAgICAgICAgICAgICAgICAgICAiaWQiOiAiMzc1ZjliYTQ1OWE0NDc3MzhmYjYw -ZmU1ZmMyNmU5YWEiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImludGVyZmFj -ZSI6ICJwdWJsaWMiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInVybCI6ICJo -dHRwOi8vMTI3LjAuMC4xOjkyOTIvdjEiLA0KICAgICAgICAgICAgICAgICAgICAg -ICAgInJlZ2lvbiI6ICJyZWdpb25PbmUiDQogICAgICAgICAgICAgICAgICAgIH0N -CiAgICAgICAgICAgICAgICBdLA0KICAgICAgICAgICAgICAgICJpZCI6ICIxNWMy -MWFhZTZiMjc0YThkYTUyZTBhMDY4ZTkwOGFhYyIsDQogICAgICAgICAgICAgICAg -InR5cGUiOiAiaW1hZ2UiLA0KICAgICAgICAgICAgICAgICJuYW1lIjogImdsYW5j -ZSINCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICB7DQogICAgICAgICAgICAg -ICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAgICAgICAgICAgew0KICAgICAg -ICAgICAgICAgICAgICAgICAgImlkIjogImVkYmQ5ZjUwZjY2NzQ2YWU5ZWQxMWRj -M2IxYWUzNWRhIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJpbnRlcmZhY2Ui -OiAiYWRtaW4iLA0KICAgICAgICAgICAgICAgICAgICAgICAgInVybCI6ICJodHRw -Oi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5 -YmI2NjE3YSIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogInJl -Z2lvbk9uZSINCiAgICAgICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAg -ICAgICAgew0KICAgICAgICAgICAgICAgICAgICAgICAgImlkIjogIjllMDNjNDZj -ODBhMzRhMTU5Y2IzOWY1Y2IwNDk4YjkyIiwNCiAgICAgICAgICAgICAgICAgICAg -ICAgICJpbnRlcmZhY2UiOiAiaW50ZXJuYWwiLA0KICAgICAgICAgICAgICAgICAg +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 -fSwNCiAgICAgICAgICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgICAgICAg -ICAgImlkIjogIjFkZjBiNDRkOTI2MzRkNTliZDBlMGQ2MGNmN2NlNDMyIiwNCiAg -ICAgICAgICAgICAgICAgICAgICAgICJpbnRlcmZhY2UiOiAicHVibGljIiwNCiAg -ICAgICAgICAgICAgICAgICAgICAgICJ1cmwiOiAiaHR0cDovLzEyNy4wLjAuMTo4 -Nzc0L3YxLjEvNjRiNmYzZmJjYzUzNDM1ZThhNjBmY2Y4OWJiNjYxN2EiLA0KICAg -ICAgICAgICAgICAgICAgICAgICAgInJlZ2lvbiI6ICJyZWdpb25PbmUiDQogICAg -ICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgICAgICBdLA0KICAgICAgICAg -ICAgICAgICJpZCI6ICIyZjQwNGZkYjg5MTU0YzU4OWVmYmMxMDcyNmIwMjllYyIs -DQogICAgICAgICAgICAgICAgInR5cGUiOiAiY29tcHV0ZSIsDQogICAgICAgICAg -ICAgICAgIm5hbWUiOiAibm92YSINCiAgICAgICAgICAgIH0sDQogICAgICAgICAg -ICB7DQogICAgICAgICAgICAgICAgImVuZHBvaW50cyI6IFsNCiAgICAgICAgICAg -ICAgICAgICAgew0KICAgICAgICAgICAgICAgICAgICAgICAgImlkIjogImE0NTAx -ZTE0MWE0YjRlMTRiZjI4MmU3YmZmZDgxZGM1IiwNCiAgICAgICAgICAgICAgICAg -ICAgICAgICJpbnRlcmZhY2UiOiAiYWRtaW4iLA0KICAgICAgICAgICAgICAgICAg -ICAgICAgInVybCI6ICJodHRwOi8vMTI3LjAuMC4xOjM1MzU3L3YzIiwNCiAgICAg -ICAgICAgICAgICAgICAgICAgICJyZWdpb24iOiAiUmVnaW9uT25lIg0KICAgICAg -ICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgICAgICB7DQogICAgICAg -ICAgICAgICAgICAgICAgICAiaWQiOiAiM2QxN2UzMjI3YmZjNDQ4M2I1OGRlNWVh -YTU4NGUzNjAiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImludGVyZmFjZSI6 -ICJpbnRlcm5hbCIsDQogICAgICAgICAgICAgICAgICAgICAgICAidXJsIjogImh0 +fQ0KICAgICAgICAgICAgICAgIF0sDQogICAgICAgICAgICAgICAgImlkIjogIjJm +NDA0ZmRiODkxNTRjNTg5ZWZiYzEwNzI2YjAyOWVjIiwNCiAgICAgICAgICAgICAg +ICAidHlwZSI6ICJjb21wdXRlIiwNCiAgICAgICAgICAgICAgICAibmFtZSI6ICJu +b3ZhIg0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAg +ICAgICAiZW5kcG9pbnRzIjogWw0KICAgICAgICAgICAgICAgICAgICB7DQogICAg +ICAgICAgICAgICAgICAgICAgICAiaWQiOiAiYTQ1MDFlMTQxYTRiNGUxNGJmMjgy +ZTdiZmZkODFkYzUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImludGVyZmFj +ZSI6ICJhZG1pbiIsDQogICAgICAgICAgICAgICAgICAgICAgICAidXJsIjogImh0 dHA6Ly8xMjcuMC4wLjE6MzUzNTcvdjMiLA0KICAgICAgICAgICAgICAgICAgICAg ICAgInJlZ2lvbiI6ICJSZWdpb25PbmUiDQogICAgICAgICAgICAgICAgICAgIH0s DQogICAgICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgICAg -ICJpZCI6ICI4Y2Q0Yjk1NzA5MGY0Y2E1ODQyYTIyZTlhNzQwOTljZCIsDQogICAg -ICAgICAgICAgICAgICAgICAgICAiaW50ZXJmYWNlIjogInB1YmxpYyIsDQogICAg -ICAgICAgICAgICAgICAgICAgICAidXJsIjogImh0dHA6Ly8xMjcuMC4wLjE6NTAw -MC92MyIsDQogICAgICAgICAgICAgICAgICAgICAgICAicmVnaW9uIjogIlJlZ2lv -bk9uZSINCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgICAgIF0s -DQogICAgICAgICAgICAgICAgImlkIjogImM1ZDkyNmQ1NjY0MjRlNGZiYTRmODBj -Mzc5MTZjZGU1IiwNCiAgICAgICAgICAgICAgICAidHlwZSI6ICJpZGVudGl0eSIs -DQogICAgICAgICAgICAgICAgIm5hbWUiOiAia2V5c3RvbmUiDQogICAgICAgICAg -ICB9DQogICAgICAgIF0sDQogICAgICAgICJ1c2VyIjogew0KICAgICAgICAgICAg -ImRvbWFpbiI6IHsNCiAgICAgICAgICAgICAgICAiaWQiOiAiZG9tYWluX2lkMSIs -DQogICAgICAgICAgICAgICAgIm5hbWUiOiAiZG9tYWluX25hbWUxIg0KICAgICAg -ICAgICAgfSwNCiAgICAgICAgICAgICJuYW1lIjogInVzZXJfbmFtZTEiLA0KICAg -ICAgICAgICAgImlkIjogInVzZXJfaWQxIg0KICAgICAgICB9DQogICAgfQ0KfQ0K -MYIByjCCAcYCAQEwgaQwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQswCQYD -VQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3RhY2sx -ETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVu -c3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZAIBETAHBgUrDgMCGjANBgkq -hkiG9w0BAQEFAASCAQCPCzpknZOfDONpHDWGrYTeyirjGGjrJem2EF2qsJ4K1x/V -guNLX1AfRnRUC95wSpGS5VCQ+OSfSFmLjJQOnMqLZ1L2MkVfn0CIkqig19sgRZ+O -hpi+0TpJ6XlCWRERJEICCOAHZ/M2iiiVFbFkIGtaJLw3HcXFreV+nEBuQSeIGH/H -FjnmocYu9vy612YT47HcyQKNMaku3QBLzFTSTiGkS4ft9yT2pNMbHZsMmysaRKWl -SfuA/DZHT6zi5D4lkxDBCexf3JAw4kOQSf/dirfDUKmIy4VPeAOuO1u86hN/coIS -JvgAJGOVUxtZCQ9256dUvKa1pLpQAgW/Ok3oPulS +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/revocation_list.pem b/examples/pki/cms/revocation_list.pem index 0e81998b8..e37f9de4a 100644 --- a/examples/pki/cms/revocation_list.pem +++ b/examples/pki/cms/revocation_list.pem @@ -1,20 +1,20 @@ -----BEGIN CMS----- -MIIDTwYJKoZIhvcNAQcCoIIDQDCCAzwCAQExCTAHBgUrDgMCGjCCAVwGCSqGSIb3 -DQEHAaCCAU0EggFJeyJyZXZva2VkIjogW3siZXhwaXJlcyI6ICIyMTEyLTA4LTE0 -VDE3OjU4OjQ4WiIsICJpZCI6ICJkYjk4ZWQyYWY2YzY3MDdiZWM2ZGM2YzY4OTI3 -ODlhMCJ9LCB7ImV4cGlyZXMiOiAiMjExMi0wOC0xNFQxNzo1ODo0OFoiLCAiaWQi -OiAiMTVjZTA1ZmQ0OTFiNzk3OTEwNjhlZDgwYTljN2Y1ZTcifSwgeyJleHBpcmVz -IjogIjIxMTItMDgtMTRUMTc6NTg6NDhaIiwgImlkIjogImRiOThlZDJhZjZjNjcw -N2JlYzZkYzZjNjg5Mjc4OWEwIn0sIHsiZXhwaXJlcyI6ICIyMTEyLTA4LTE0VDE3 -OjU4OjQ4WiIsICJpZCI6ICIxNWNlMDVmZDQ5MWI3OTc5MTA2OGVkODBhOWM3ZjVl -NyJ9XX0xggHKMIIBxgIBATCBpDCBnjEKMAgGA1UEBRMBNTELMAkGA1UEBhMCVVMx -CzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlTdW5ueXZhbGUxEjAQBgNVBAoTCU9wZW5T -dGFjazERMA8GA1UECxMIS2V5c3RvbmUxJTAjBgkqhkiG9w0BCQEWFmtleXN0b25l -QG9wZW5zdGFjay5vcmcxFDASBgNVBAMTC1NlbGYgU2lnbmVkAgERMAcGBSsOAwIa -MA0GCSqGSIb3DQEBAQUABIIBAGn4kryxJudTZYMf32gKnoNHeAXRb97CoCXiTgs2 -gu/blX/fwMdrL8GLg2puYR07XBgjo56vMsD94ZIRyhcS1lFti9veQHt7Xp8kbR8l -nbx9fsOhMxUHLRnxioieA9T1ykP8ZvYV3hYCeXkIYhPgD4lAAAmNq99ZxBRS3csE -DP+Xz1+UYvT6Qm/NWRuj7WIjofneIB7gT6L5irsU0qtMCQeqI3dsP9GSsy4HJvBR -BBIzQ7fEMRCGTADbk4ml+6Dx+Jm5SO80NvinzxCjO3DbkcEG1pQ3RGVEn3gyzg2a -ssaRU4ycbYACA99K5UzCtSj8glGXFa1cnx42nSn2LbfJP1M= +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/gen_cmsz.py b/examples/pki/gen_cmsz.py index 6840c08e5..35a6a8f8c 100644 --- a/examples/pki/gen_cmsz.py +++ b/examples/pki/gen_cmsz.py @@ -12,9 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. -import json import os +from oslo_serialization import jsonutils + from keystoneclient.common import cms from keystoneclient import utils @@ -44,7 +45,7 @@ def generate_revocation_list(): 'id': id, "expires": "2112-08-14T17:58:48Z" }) - revoked_json = json.dumps({"revoked": revoked_list}) + 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, @@ -83,6 +84,7 @@ def generate_der_form(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') @@ -91,12 +93,12 @@ def generate_der_form(name): # validate the JSON try: - token_data = json.loads(string_data) + 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 = json.dumps(token_data).encode('utf-8') + text = jsonutils.dumps(token_data).encode('utf-8') # Uncomment to record the token uncompressed, # useful for debugging diff --git a/examples/pki/gen_pki.sh b/examples/pki/gen_pki.sh index b8b28f9dc..f3a3fddb0 100755 --- a/examples/pki/gen_pki.sh +++ b/examples/pki/gen_pki.sh @@ -42,7 +42,7 @@ function generate_ca_conf { [ req ] default_bits = 2048 default_keyfile = cakey.pem -default_md = default +default_md = sha256 prompt = no distinguished_name = ca_distinguished_name @@ -69,7 +69,7 @@ function generate_ssl_req_conf { [ req ] default_bits = 2048 default_keyfile = keystonekey.pem -default_md = default +default_md = sha256 prompt = no distinguished_name = distinguished_name @@ -90,7 +90,7 @@ function generate_cms_signing_req_conf { [ req ] default_bits = 2048 default_keyfile = keystonekey.pem -default_md = default +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 = default +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:2048 -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:2048 -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:2048 -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,11 +191,6 @@ 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 diff --git a/examples/pki/private/cakey.pem b/examples/pki/private/cakey.pem index 1c93ee18c..9c204c500 100644 --- a/examples/pki/private/cakey.pem +++ b/examples/pki/private/cakey.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCl8906EaRpibQF -cCBWfxzLi5x/XpZ9iL6UX92NrSJxcDbaGws7s+GtjgDy8UOEonesRWTeqQEZtHpC -3/UHHOnsA8F6ha/pq9LioqT7RehCnZCLBJwh5Ct+lclpWs15SkjJD2LTDkjox0eA -9nOBx+XDlWyU/GAyqx5Wsvg/Kxr0iod9/4IcJdnSdUjq4v0Cxg/zNk08XPJX+F0b -UDhgdUf7JrAmmS5LA8wphRnbIgtVsf6VN9HrbqtHAJDxh8gEfuwdhEW1df1fBtZ+ -6WMIF3IRSbIsZELFB6sqcyRj7HhMoWMkdEyPb2f8mq61MzTgE6lJGIyTRvEoFie7 -qtGADIofAgMBAAECggEBAJ47X3y2xaU7f0KQHsVafgI2JAnuDl+zusOOhJlJs8Wl -0Sc1EgjjAxOQiqcaE96rap//qqYDTuFLjCenkuItV32KNzizr3+GLZWaruRHS6X4 -xpFG2/gUrsQL3fdudOxpP+01lmzW+f25xRvZ4VilWRabquSDntWxA0R3cOwKFbGD -uuwbTw3pBrRfCk/2IdpQtRrvvkVIFiYT6b/zeCQzhp4RETbC0oxqcEEOIUGmimAV -9cbwafinxCo54cOfX4JAh3j7Mp3eQUymoFk5gnmIeVe0QmpH2VkN7eItrhEvHKOk -On7a5xvQ8s3wqPV5ZawHQcqar/p3QnGkiT6a+8LkIMECgYEA2iJ2DprTGZFRN0M7 -Yj4WLsSC3/GKK8eYsKG3TvMrmPqUDaiWLIvBoc1Le59x9eoF7Mha+WX+cAFL+GTg -1sB+PUZZStpf1R1tGvMldvpQ+5GplUBpuQe4J0n5rCG6+5jkvSr7xO+G1B+C3GFq -KR3iltiW5WJRVwh2k8yGvx3agyUCgYEAwsKFX82F7O+9IVud1JSQWmZMiyEK+DEX -JRnwx4HBuWr+AZqbb0grRRb6x8JTUOD4T7DZGxTaAdfzzRjKU2sBAO8VCgaj2Auv -5nsbvfXvrmDDCqwoaD2PMy+kgFvE0QTh65tzuGXl1IgpIYSC1JwnP6kOeUDbqE+k -UXzfVZzDdvMCgYByk9dfJIPt0h7O4Em4+NO+DQqRhtYE2PqjDM60cZZc7IIICp2X -GHHFA4i6jq3Vde9WyIbAqYpUWtoExzgylTm6BdGxN7NOxf4hQcZUEHepLIHfG85s -mlloibrTZ4RH06+SjZlhgE9Z7JNYHvMcVc5HXc0k/9ep15AxYiUFDjFQ4QKBgG7i -k089U4/X2wWgBNdgkmN1tQTNllJCmNvdzhG41dQ8j0vYe8C7BS+76qJLCGaW/6lX -lfRuRcUg78UI5UDjPloKxR7FMwmxdb+yvdPEr2bH3qQ36nWW/u30pSMTnJYownwD -MLp/AYCk2U4lBNwJ3+rF1ODCRY2pcnOWtg0nSL5zAoGAWRoOinogEnOodJzO7eB3 -TmL6M9QMyrAPBDsCnduJ8yW5mMUNod139YbSDxZPYwTLhK/GiHP/7OvLV5hg0s4s -QKnNaMeEowX7dyEO4ehnbfzysxXPKLRVhWhN6MCUc71NMxqr7QkuCXAjJS6/G21+ -Im3+Xb3Scq+UZghR+jiEZF0= +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 758c0ffe1..ddf3066d5 100644 --- a/examples/pki/private/signing_key.pem +++ b/examples/pki/private/signing_key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDM+VrILLl962VH -S8EKWVzdkaOy0OoxGZ63gajM7VTm8AbgtVnYibIOnVZQuz1XbftIGNXPFhYNUypr -LnMXrEEsnxgD4PvU/4bETG+stdricX6d1oKqsNFNR7F7zImiR/OzGhp7dONwccxf -kfX4QHA5Ogso+XMfSdC72SRDszeCeGUcjuo/w2WSLW95SuVvcZLqE/pk3Q2TkCZ1 -8hvNfLoln43QpC469a7srUXATqOJ2mPNvL6E/wOyPefmAoCoG44lFoR3k2jZjBEI -hstJxmH7XgvqErBzpcWd29dms8xz5PNwYdns9CIfb3GaHvQ6r5RTl37/avDrGHOW -KOoD01xLAgMBAAECggEAaIi22qWsh+JYCW9B6NRAPyN6V8Sh2x6UykOO4cwb45b/ -+vOh+YPn0fo9vfhvxTnq0A8SY4WBA5SpanYK7kTEDEyqw7em1y7l/RB6V5t7IMb+ -6uIuS3zXkVEB3AApJSEK0Ql7/gBTydHPh+H5jnzWfujyLhhhtNBBarvH+drZcWio -lWx8RERN4cH+3DZD/xxjH2Ff+X1XMvb8Xcup7MlWi2FtREg7LttLNWNK25iWjciP -QwfWQIrURRJrD2IrOr9V2nuIEvRqRRBoO+pxJT2sC48NJ3hiKV2GtSQe2nRpQJ47 -f9MEsF5KVQOOn+aQ60EKOI0MpNPmpiCZ5hFvBrNuOQKBgQD6vueEdI9eJgz5YN+t -XWdpNippv35RTD8R4bQcE6GqIUXOmtQFS2wPJLn7nisZUsGMNEs36Yl0T9iow63r -5GNAfgzpqN1XZqaSMwAdxKmlBNYpAkVXHhv+1jN+9diDYmoj9T+3Q6Zvk5e/Liyp -6i+TsDppwmmr2utWajhyJ7owFwKBgQDRROncTztGDYLfRcrIoYsPo79KQ8tqwd2a -07Usch2kplTqojCUmmhMMFgV2eZPPiCjnEy2bAYh9I/oj7xG6EwApXTshZdCpivC -rbUV64MakRTUP8IvM6PdI+apkJRsRUi/bSyIbcRlvEoCMNZhfj/5VY6w/jlwrPJj -oBOCXBlB7QKBgQDGEbEeX1i03UfYYh6uep7qbEAaooqsu5cCkBDPMO6+TmQvLPyY -Zhio6bEEQs/2w/lhwBk+xHqw5zXVMiWbtiB03F1k4eBeXxbrW+AWo7gCQ4zMfh+6 -Dm284wVwn9D1D/OaDevT31uEvcjb2ySq3/PPLSEnU8xXVaoa6/NEsX8Q5wKBgQCm -2smULWBXZKJ6n00mVxdnqun0rsVcI6Mrta14+KwGAdEnG5achdivFsTE924YtLKV -gSPxN4RUQokTprc52jHvOf1WMNYAADpYCOSfy55G6nKvIP8VX5lB00Qw4uRUx5FP -gB7H0K2NaGmiAYqNRXqAtOUG3kyyOFMzeAjWIdTJqQKBgQCHzY1c7sS1vv7mPEkr -6CpwoaEbZeFnWoHBA8Rd82psqfYsVJIRwk5Id8zgDSEmoEi8hQ9UrYbrFpLK77xq -EYSxLQHTNlM0G3lyEsv/gJhwYYhdTYiW3Cx3F6Y++jyn9O/+hFMyQvuesAL7DUYE -ptEfvzFprpQUpByXkIpuJub6fg== +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 363ce94bd..d339ea213 100644 --- a/examples/pki/private/ssl_key.pem +++ b/examples/pki/private/ssl_key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDL06AaJROwHPgJ -9tcySSBepzJ81jYars2sMvLjyuvdiIBbhWvbS/a9Tw3WgL8H6OALkHiOU/f0A6Rp -v8dGDIDsxZQVjT/4SLaQUOeDM+9bfkKHpSd9G3CsdSSZgOH08n+MyZ7slPHfUHLY -Wso0SJD0vAi1gmGDlSM/mmhhHTpCDGo6Wbwqare6JNeTCGJTJYwrxtoMCh/W1Zrs -lPC5lFvlHD7KBBf6IU2A8Xh/dUa3p5pmQeHPW8Em90DzIB1qH0DRXl3KANc24xYR -R45pPCVkk6vFsy6P0JwwpnkszB+LcK6CEsJhLsOYvQFsiQfSZ8m7YGhgrMLxtop4 -YEPirGGrAgMBAAECggEATwvbY0hNwlb5uqOIAXBqpUqiQdexU9fG26lGmSDxKBDv -9o5frcRgBDrMWwvDCgY+HT4CAvB9kJx4/qnpVjkzJp/ZNiJ5VIiehIlbv348rXbh -xkk+bz5dDATCFOXuu1fwL2FhyM5anwhMAav0DyK1VLQ3jGzr9GO6L8hqAn+bQFFu -6ngiODwfhBMl5aRoL9UOBEhccK07znrH0JGRz+3+5Cdz59Xw91Bv210LhNNDL58+ -0JD0N+YztVOQd2bgwo0bQbOEijzmYq+0mjoqAnJh1/++y7PlIPs0AnPgqSnFPx9+ -6FsQEVRgk5Uq3kvPLaP4nT2y6MDZSp+ujYldvJhyQQKBgQDuX2pZIJMZ4aFnkG+K -TmJ5wsLa/u9an0TmvAL9RLtBpVpQNKD8cQ+y8PUZavXDbAIt5NWqZVnTbCR79Dnd -mZKblwcHhtsyA5f89el5KcxY2BREWdHdTnJpNd7XRlUECmzvX1zGj77lA982PhII -yflRBRV3vqLkgC8vfoYgRyRElwKBgQDa5jnLdx/RahfYMOgn1HE5o4hMzLR4Y0Dd -+gELshcUbPqouoP5zOb8WOagVJIgZVOSN+/VqbilVYrqRiNTn2rnoxs+HHRdaJNN -3eXllD4J2HfC2BIj1xSpIdyh2XewAJqw9IToHNB29QUhxOtgwseHciPG6JaKH2ik -kqGKH/EKDQKBgFFAftygiOPCkCTgC9UmANUmOQsy6N2H+pF3tsEj43xt44oBVnqW -A1boYXNnjRwuvdNs9BPf9i1l6E3EItFRXrLgWQoMwryakv0ryYh+YeRKyyW9RBbe -fYs1TJ8unx4Ae79gTxxztQsVNcmkgLs0NWKTjAzEE3w14V+cDhYEie1DAoGBAJdI -V5cLrBzBstsB6eBlDR9lqrRRIUS2a8U9m+1mVlcSfiWQSdehSd4K3tDdwePLw3ch -W4qR8n+pYAlLEe0gFvUhn5lMdwt7U5qUCeehjUKmrRYm2FqWsbu2IFJnBjXIJSC4 -zQXRrC0aZ0KQYpAL7XPpaVp1slyhGmPqxuO78Y0dAoGBAMHo3EIMwu9rfuGwFodr -GFsOZhfJqgo5GDNxxf89Q9WWpMDTCdX+wdBTrN/wsMbBuwIDHrUuRnk6D5CWRjSk -/ikCgHN3kOtrbL8zzqRomGAIIWKYGFEIGe1GHVGo5r//HXHdPxFXygvruQ/xbOA4 -RGvmDiji8vVDq7Shho8I6KuT +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 index ba2f0b6e3..2438ec7c8 100755 --- a/examples/pki/run_all.sh +++ b/examples/pki/run_all.sh @@ -26,6 +26,5 @@ generate_ca ssl_cert_req cms_signing_cert_req issue_certs -create_middleware_cert gen_sample_cms cleanup diff --git a/keystoneclient/__init__.py b/keystoneclient/__init__.py index 96f32ab71..e8ef58ebc 100644 --- a/keystoneclient/__init__.py +++ b/keystoneclient/__init__.py @@ -27,6 +27,7 @@ """ +import importlib import sys import pbr.version @@ -34,7 +35,7 @@ __version__ = pbr.version.VersionInfo('python-keystoneclient').version_string() -__all__ = [ +__all__ = ( # Modules 'generic', 'v2_0', @@ -46,7 +47,7 @@ 'exceptions', 'httpclient', 'service_catalog', -] +) class _LazyImporter(object): @@ -67,10 +68,9 @@ def __getattr__(self, name): 'v2_0', 'v3', ] - # __import__ rather than importlib for Python 2.6. if name in lazy_submodules: - __import__('keystoneclient.%s' % name) - return getattr(self, name) + return importlib.import_module('keystoneclient.%s' % name) + # Return module attributes like __all__ etc. return getattr(self._module, name) diff --git a/keystoneclient/_discover.py b/keystoneclient/_discover.py index 07d0ae6c6..a27236cdc 100644 --- a/keystoneclient/_discover.py +++ b/keystoneclient/_discover.py @@ -25,14 +25,12 @@ import re from keystoneclient import exceptions -from keystoneclient.i18n import _, _LI, _LW -from keystoneclient import utils +from keystoneclient.i18n import _ _LOGGER = logging.getLogger(__name__) -@utils.positional() def get_version_data(session, url, authenticated=None): """Retrieve raw version data from a url.""" headers = {'Accept': 'application/json'} @@ -41,28 +39,30 @@ def get_version_data(session, url, authenticated=None): try: body_resp = resp.json() - except ValueError: + 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): + 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: + 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: + except KeyError: # nosec(cjschaef): raise a DiscoveryFailure pass err_text = resp.text[:50] + '...' if len(resp.text) > 50 else resp.text @@ -72,18 +72,19 @@ def get_version_data(session, url, authenticated=None): 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: + 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: + except Exception: # nosec(cjschaef): 'version' is not a float, try a + # different type or raise a TypeError pass else: version = str(num) @@ -91,21 +92,22 @@ def normalize_version_number(version): # if it's a string (or an integer) from above break it on . try: return tuple(map(int, version.split('.'))) - except Exception: + 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: + 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 is a suitable match for a required - version. + """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. @@ -136,7 +138,6 @@ class Discover(object): DEPRECATED_STATUSES = ('deprecated',) EXPERIMENTAL_STATUSES = ('experimental',) - @utils.positional() def __init__(self, session, url, authenticated=None): self._data = get_version_data(session, url, authenticated=authenticated) @@ -162,8 +163,8 @@ def raw_version_data(self, allow_experimental=False, try: status = v['status'] except KeyError: - _LOGGER.warning(_LW('Skipping over invalid version data. ' - 'No stability status in version.')) + _LOGGER.warning('Skipping over invalid version data. ' + 'No stability status in version.') continue status = status.lower() @@ -195,6 +196,9 @@ def version_data(self, **kwargs): :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 = [] @@ -202,14 +206,13 @@ def version_data(self, **kwargs): try: version_str = v['id'] except KeyError: - _LOGGER.info(_LI('Skipping invalid version data. Missing ID.')) + _LOGGER.info('Skipping invalid version data. Missing ID.') continue try: links = v['links'] except KeyError: - _LOGGER.info( - _LI('Skipping invalid version data. Missing links')) + _LOGGER.info('Skipping invalid version data. Missing links') continue version_number = normalize_version_number(version_str) @@ -219,15 +222,15 @@ def version_data(self, **kwargs): rel = link['rel'] url = link['href'] except (KeyError, TypeError): - _LOGGER.info(_LI('Skipping invalid version link. ' - 'Missing link URL or relationship.')) + _LOGGER.info('Skipping invalid version link. ' + 'Missing link URL or relationship.') continue if rel.lower() == 'self': break else: - _LOGGER.info(_LI('Skipping invalid version data. ' - 'Missing link to endpoint.')) + _LOGGER.info('Skipping invalid version data. ' + 'Missing link to endpoint.') continue versions.append({'version': version_number, diff --git a/keystoneclient/access.py b/keystoneclient/access.py index 009b72e0e..a93da0a1e 100644 --- a/keystoneclient/access.py +++ b/keystoneclient/access.py @@ -16,6 +16,7 @@ import datetime +import warnings from oslo_utils import timeutils @@ -37,12 +38,22 @@ class AccessInfo(dict): @classmethod def factory(cls, resp=None, body=None, region_name=None, auth_token=None, **kwargs): - """Create AccessInfo object given a successful auth response & body - or a user-provided dict. + """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. + """ - # FIXME(jamielennox): Passing region_name is deprecated. Provide an - # appropriate warning. - auth_ref = None + 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): @@ -83,7 +94,7 @@ 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. :returns: true if expiration is within the given duration :rtype: boolean @@ -100,8 +111,9 @@ def will_expire_soon(self, stale_duration=None): @classmethod def is_valid(cls, body, **kwargs): - """Determines if processing v2 or v3 token given a successful - auth body or a user-provided dict. + """Determine if processing valid v2 or v3 token. + + Validates from the auth body or a user-provided dict. :returns: true if auth body matches implementing class :rtype: boolean @@ -109,7 +121,7 @@ def is_valid(cls, body, **kwargs): raise NotImplementedError() def has_service_catalog(self): - """Returns true if the authorization token has a service catalog. + """Return true if the authorization token has a service catalog. :returns: boolean """ @@ -117,8 +129,9 @@ def has_service_catalog(self): @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 """ @@ -132,12 +145,12 @@ def auth_token(self, value): def auth_token(self): try: del self['auth_token'] - except KeyError: + except KeyError: # nosec(cjschaef): 'auth_token' is not in the dict pass @property def expires(self): - """Returns the token expiration (as datetime object) + """Return the token expiration (as datetime object). :returns: datetime """ @@ -145,7 +158,7 @@ def expires(self): @property def issued(self): - """Returns the token issue time (as datetime object) + """Return the token issue time (as datetime object). :returns: datetime """ @@ -153,7 +166,8 @@ def issued(self): @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. @@ -164,7 +178,7 @@ def username(self): @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 """ @@ -172,8 +186,7 @@ def user_id(self): @property def user_domain_id(self): - """Returns the domain id of the user associated with the authentication - request. + """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. @@ -184,8 +197,7 @@ def user_domain_id(self): @property def user_domain_name(self): - """Returns the domain name of the user associated with the - authentication request. + """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. @@ -196,8 +208,7 @@ def user_domain_name(self): @property def role_ids(self): - """Returns a list of role ids of the user associated with the - authentication request. + """Return a list of user's role ids associated with the auth request. :returns: a list of strings of role ids """ @@ -205,8 +216,7 @@ def role_ids(self): @property def role_names(self): - """Returns a list of role names of the user associated with the - authentication request. + """Return a list of user's role names associated with the auth request. :returns: a list of strings of role names """ @@ -214,7 +224,7 @@ def role_names(self): @property def domain_name(self): - """Returns the domain name associated with the authentication token. + """Return the domain name associated with the auth request. :returns: str or None (if no domain associated with the token) """ @@ -222,7 +232,7 @@ def domain_name(self): @property def domain_id(self): - """Returns the domain id associated with the authentication token. + """Return the domain id associated with the auth request. :returns: str or None (if no domain associated with the token) """ @@ -230,7 +240,7 @@ def domain_id(self): @property def project_name(self): - """Returns the project name associated with the authentication request. + """Return the project name associated with the auth request. :returns: str or None (if no project associated with the token) """ @@ -243,10 +253,15 @@ def tenant_name(self): @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. - This is deprecated, use project_scoped instead. + 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 """ @@ -254,8 +269,7 @@ def scoped(self): @property def project_scoped(self): - """Returns true if the authorization token was scoped to a tenant - (project). + """Return true if the auth token was scoped to a tenant(project). :returns: bool """ @@ -263,7 +277,7 @@ def project_scoped(self): @property def domain_scoped(self): - """Returns true if the authorization token was scoped to a domain. + """Return true if the auth token was scoped to a domain. :returns: bool """ @@ -271,7 +285,7 @@ def domain_scoped(self): @property def trust_id(self): - """Returns the trust id associated with the authentication token. + """Return the trust id associated with the auth request. :returns: str or None (if no trust associated with the token) """ @@ -279,8 +293,9 @@ def trust_id(self): @property def trust_scoped(self): - """Returns true if the authorization token was scoped as delegated in a - trust, via the OS-TRUST v3 extension. + """Return true if the auth token was scoped from a delegated trust. + + The trust delegation is via the OS-TRUST v3 extension. :returns: bool """ @@ -288,7 +303,7 @@ def trust_scoped(self): @property def trustee_user_id(self): - """Returns the trustee user id associated with a trust. + """Return the trustee user id associated with a trust. :returns: str or None (if no trust associated with the token) """ @@ -296,7 +311,7 @@ def trustee_user_id(self): @property def trustor_user_id(self): - """Returns the trustor user id associated with a trust. + """Return the trustor user id associated with a trust. :returns: str or None (if no trust associated with the token) """ @@ -304,9 +319,9 @@ def trustor_user_id(self): @property def project_id(self): - """Returns the project ID associated with the authentication - request, or None if the authentication request wasn't scoped to a - project. + """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) """ @@ -319,8 +334,7 @@ def tenant_id(self): @property def project_domain_id(self): - """Returns the domain id of the project associated with the - authentication request. + """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. @@ -331,8 +345,7 @@ def project_domain_id(self): @property def project_domain_name(self): - """Returns the domain name of the project associated with the - authentication request. + """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. @@ -343,13 +356,16 @@ def project_domain_name(self): @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. + it from the service catalog yourself. This may be removed in the 2.0.0 + release. :returns: tuple of urls """ @@ -357,12 +373,15 @@ def auth_url(self): @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. + it from the service catalog yourself. This may be removed in the 2.0.0 + release. :returns: tuple of urls """ @@ -370,7 +389,7 @@ def management_url(self): @property def version(self): - """Returns the version of the auth token from identity service. + """Return the version of the auth token from identity service. :returns: str """ @@ -394,7 +413,7 @@ def oauth_consumer_id(self): @property def is_federated(self): - """Returns true if federation was used to get the token. + """Return true if federation was used to get the token. :returns: boolean """ @@ -431,9 +450,7 @@ def initial_audit_id(self): class AccessInfoV2(AccessInfo): - """An object for encapsulating a raw v2 auth token from identity - service. - """ + """An object for encapsulating raw v2 auth token from identity service.""" def __init__(self, *args, **kwargs): super(AccessInfo, self).__init__(*args, **kwargs) @@ -506,7 +523,8 @@ def domain_id(self): def project_name(self): try: tenant_dict = self['token']['tenant'] - except KeyError: + 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') @@ -514,20 +532,31 @@ def project_name(self): # pre grizzly try: return self['user']['tenantName'] - except KeyError: + 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: + except KeyError: # nosec(cjschaef): no 'token' key or 'tenantName' or + # 'tenantId' could be found, return None pass @property def scoped(self): - if ('serviceCatalog' in self - and self['serviceCatalog'] - and 'tenant' in self['token']): + """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 @@ -560,7 +589,8 @@ def trustor_user_id(self): def project_id(self): try: tenant_dict = self['token']['tenant'] - except KeyError: + except KeyError: # nosec(cjschaef): no 'token' key or 'tenant' dict, + # attempt to return 'tenantId' or return None pass else: return tenant_dict.get('id') @@ -568,13 +598,15 @@ def project_id(self): # pre grizzly try: return self['user']['tenantId'] - except KeyError: + 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: + except KeyError: # nosec(cjschaef): no 'token' key or 'tenantId' + # could be found, return None pass @property @@ -589,8 +621,15 @@ def project_domain_name(self): @property def auth_url(self): - # FIXME(jamielennox): this is deprecated in favour of retrieving it - # from the service catalog. Provide a warning. + """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', @@ -600,8 +639,15 @@ def auth_url(self): @property def management_url(self): - # FIXME(jamielennox): this is deprecated in favour of retrieving it - # from the service catalog. Provide a warning. + """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', @@ -637,9 +683,7 @@ def audit_chain_id(self): class AccessInfoV3(AccessInfo): - """An object for encapsulating a raw v3 auth token from identity - service. - """ + """An object encapsulating raw v3 auth token from identity service.""" def __init__(self, token, *args, **kwargs): super(AccessInfo, self).__init__(*args, **kwargs) @@ -747,6 +791,15 @@ def project_name(self): @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 @@ -775,8 +828,15 @@ def trustor_user_id(self): @property def auth_url(self): - # FIXME(jamielennox): this is deprecated in favour of retrieving it - # from the service catalog. Provide a warning. + """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', @@ -786,8 +846,15 @@ def auth_url(self): @property def management_url(self): - # FIXME(jamielennox): this is deprecated in favour of retrieving it - # from the service catalog. Provide a warning. + """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', diff --git a/keystoneclient/adapter.py b/keystoneclient/adapter.py index 74399da17..94cd81dc6 100644 --- a/keystoneclient/adapter.py +++ b/keystoneclient/adapter.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_serialization import jsonutils +import warnings -from keystoneclient import utils +from oslo_serialization import jsonutils class Adapter(object): @@ -45,11 +45,15 @@ class Adapter(object): :type logger: logging.Logger """ - @utils.positional() def __init__(self, session, service_type=None, service_name=None, interface=None, region_name=None, endpoint_override=None, version=None, auth=None, user_agent=None, connect_retries=None, logger=None): + 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 @@ -200,7 +204,8 @@ def request(self, *args, **kwargs): try: kwargs['json'] = kwargs.pop('body') - except KeyError: + 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) @@ -209,7 +214,8 @@ def request(self, *args, **kwargs): if resp.text: try: body = jsonutils.loads(resp.text) - except ValueError: + except ValueError: # nosec(cjschaef): return None for body as + # expected pass return resp, body diff --git a/keystoneclient/apiclient/__init__.py b/keystoneclient/apiclient/__init__.py deleted file mode 100644 index 344b66169..000000000 --- a/keystoneclient/apiclient/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import warnings - -from keystoneclient import exceptions - -# NOTE(akurilin): Module 'keystoneclient.apiclient' contains only exceptions -# which are deprecated, so this module must also be deprecated which helps -# to report 'deprecated' status of exceptions for next kind of imports -# from keystoneclient.apiclient import exceptions - -warnings.warn("The 'keystoneclient.apiclient' module is deprecated since " - "v.0.7.1. Use 'keystoneclient.exceptions' instead of this " - "module.", DeprecationWarning) - -__all__ = [ - 'exceptions', -] diff --git a/keystoneclient/auth/__init__.py b/keystoneclient/auth/__init__.py index 3b761c29b..c9acef819 100644 --- a/keystoneclient/auth/__init__.py +++ b/keystoneclient/auth/__init__.py @@ -10,13 +10,14 @@ # 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__ = [ +__all__ = ( # auth.base 'AUTH_INTERFACE', 'BaseAuthPlugin', @@ -35,4 +36,4 @@ 'get_plugin_options', 'load_from_conf_options', 'register_conf_options', -] +) diff --git a/keystoneclient/auth/base.py b/keystoneclient/auth/base.py index 4d8b84003..b6753cdf1 100644 --- a/keystoneclient/auth/base.py +++ b/keystoneclient/auth/base.py @@ -12,7 +12,8 @@ import os -import six +from debtcollector import removals +from keystoneauth1 import plugin import stevedore from keystoneclient import exceptions @@ -21,12 +22,17 @@ # NOTE(jamielennox): The AUTH_INTERFACE is a special value that can be # requested from get_endpoint. If a plugin receives this as the value of # 'interface' it should return the initial URL that was passed to the plugin. -AUTH_INTERFACE = object() +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. @@ -41,6 +47,11 @@ def get_available_plugin_names(): 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. @@ -55,6 +66,11 @@ class as the value. 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. @@ -168,6 +184,19 @@ def get_endpoint(self, session, **kwargs): """ 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. @@ -222,7 +251,7 @@ def get_options(cls): :returns: A list of Param objects describing available plugin parameters. - :rtype: list + :rtype: List """ return [] @@ -246,7 +275,6 @@ def register_argparse_arguments(cls, 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 @@ -263,7 +291,7 @@ def register_argparse_arguments(cls, parser): # select the first ENV that is not false-y or return None env_vars = (os.environ.get(e) for e in envs) - default = six.next(six.moves.filter(None, env_vars), None) + default = next(filter(None, env_vars), None) parser.add_argument(*args, default=default or opt.default, @@ -283,7 +311,6 @@ def load_from_argparse_arguments(cls, namespace, **kwargs): :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) @@ -313,7 +340,6 @@ def load_from_conf_options(cls, conf, group, **kwargs): :returns: An authentication Plugin. :rtype: :py:class:`keystoneclient.auth.BaseAuthPlugin` """ - def _getter(opt): return conf[group][opt.dest] @@ -321,7 +347,7 @@ def _getter(opt): @classmethod def load_from_options_getter(cls, getter, **kwargs): - """Load a plugin from a getter function that returns appropriate values + """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 @@ -336,7 +362,6 @@ def load_from_options_getter(cls, getter, **kwargs): :returns: An authentication Plugin. :rtype: :py:class:`keystoneclient.auth.BaseAuthPlugin` """ - plugin_opts = cls.get_options() for opt in plugin_opts: diff --git a/keystoneclient/auth/cli.py b/keystoneclient/auth/cli.py index 40a81c1d4..d18baec7e 100644 --- a/keystoneclient/auth/cli.py +++ b/keystoneclient/auth/cli.py @@ -13,11 +13,16 @@ import argparse import os +from debtcollector import removals + from keystoneclient.auth import base -from keystoneclient import utils -@utils.positional() +@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. @@ -25,7 +30,7 @@ def register_argparse_arguments(parser, argv, default=None): the options required for that specific plugin if available. :param argparse.ArgumentParser: the parser to attach argparse options to. - :param list argv: the arguments provided to the appliation. + :param 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. @@ -60,6 +65,11 @@ def register_argparse_arguments(parser, argv, default=None): 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. diff --git a/keystoneclient/auth/conf.py b/keystoneclient/auth/conf.py index b61c123c6..ca3cbcf8c 100644 --- a/keystoneclient/auth/conf.py +++ b/keystoneclient/auth/conf.py @@ -10,6 +10,7 @@ # 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 @@ -20,6 +21,11 @@ _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. @@ -27,7 +33,7 @@ def get_common_conf_options(): or to manipulate the options before registering them yourself. The options that are set are: - :auth_plugin: The name of the pluign to load. + :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. @@ -35,6 +41,11 @@ def get_common_conf_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. @@ -46,6 +57,11 @@ def get_plugin_options(name): 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. @@ -77,6 +93,11 @@ def register_conf_options(conf, group): 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. diff --git a/keystoneclient/auth/identity/__init__.py b/keystoneclient/auth/identity/__init__.py index d2aca8f9d..8146c1e58 100644 --- a/keystoneclient/auth/identity/__init__.py +++ b/keystoneclient/auth/identity/__init__.py @@ -28,10 +28,10 @@ Token = generic.Token -__all__ = ['BaseIdentityPlugin', +__all__ = ('BaseIdentityPlugin', 'Password', 'Token', 'V2Password', 'V2Token', 'V3Password', - 'V3Token'] + 'V3Token') diff --git a/keystoneclient/auth/identity/access.py b/keystoneclient/auth/identity/access.py index 46df3bfd1..3e096b7af 100644 --- a/keystoneclient/auth/identity/access.py +++ b/keystoneclient/auth/identity/access.py @@ -11,7 +11,6 @@ # under the License. from keystoneclient.auth.identity import base -from keystoneclient import utils class AccessInfoPlugin(base.BaseIdentityPlugin): @@ -31,7 +30,6 @@ class AccessInfoPlugin(base.BaseIdentityPlugin): if using the AUTH_INTERFACE with get_endpoint. (optional) """ - @utils.positional() def __init__(self, auth_ref, auth_url=None): super(AccessInfoPlugin, self).__init__(auth_url=auth_url, reauthenticate=False) diff --git a/keystoneclient/auth/identity/base.py b/keystoneclient/auth/identity/base.py index f7851926b..b27d349e6 100644 --- a/keystoneclient/auth/identity/base.py +++ b/keystoneclient/auth/identity/base.py @@ -12,15 +12,14 @@ import abc import logging +import threading +import warnings from oslo_config import cfg -import six from keystoneclient import _discover from keystoneclient.auth import base from keystoneclient import exceptions -from keystoneclient.i18n import _LW -from keystoneclient import utils LOG = logging.getLogger(__name__) @@ -31,8 +30,7 @@ def get_options(): ] -@six.add_metaclass(abc.ABCMeta) -class BaseIdentityPlugin(base.BaseAuthPlugin): +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 @@ -48,18 +46,118 @@ def __init__(self, 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) - # NOTE(jamielennox): DEPRECATED. The following should not really be set - # here but handled by the individual auth plugin. - self.username = username - self.password = password - self.token = token - self.trust_id = trust_id + self._trust_id = value @abc.abstractmethod def get_auth_ref(self, session, **kwargs): @@ -67,13 +165,13 @@ def get_auth_ref(self, session, **kwargs): This method is overridden by the various token version plugins. - This function should not be called independently and is expected to be - invoked via the do_authenticate function. + This method should not be called independently and is expected to be + invoked via the do_authenticate() method. - This function will be invoked if the AcessInfo object cached by the + 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. + 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 @@ -87,6 +185,7 @@ def get_auth_ref(self, session, **kwargs): :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. @@ -141,8 +240,14 @@ def get_access(self, session, **kwargs): :returns: Valid AccessInfo :rtype: :py:class:`keystoneclient.access.AccessInfo` """ - if self._needs_reauthenticate(): - self.auth_ref = self.get_auth_ref(session) + # 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 @@ -208,10 +313,10 @@ def get_endpoint(self, session, service_type=None, interface=None, else: if not service_type: - LOG.warning(_LW( + LOG.warning( 'Plugin cannot return an endpoint without knowing the ' 'service type that is required. Add service_type to ' - 'endpoint filtering data.')) + 'endpoint filtering data.') return None if not interface: @@ -244,10 +349,9 @@ def get_endpoint(self, session, service_type=None, interface=None, # 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(_LW( + LOG.warning( 'Failed to contact the endpoint at %s for discovery. Fallback ' - 'to using that endpoint as the base url.'), - url) + 'to using that endpoint as the base url.', url) else: url = disc.url_for(version) @@ -259,7 +363,6 @@ def get_user_id(self, session, **kwargs): def get_project_id(self, session, **kwargs): return self.get_access(session).project_id - @utils.positional() def get_discovery(self, session, url, authenticated=None): """Return the discovery object for a URL. diff --git a/keystoneclient/auth/identity/generic/__init__.py b/keystoneclient/auth/identity/generic/__init__.py index b24c3d642..a96fb9791 100644 --- a/keystoneclient/auth/identity/generic/__init__.py +++ b/keystoneclient/auth/identity/generic/__init__.py @@ -15,7 +15,7 @@ from keystoneclient.auth.identity.generic.token import Token # noqa -__all__ = ['BaseGenericPlugin', +__all__ = ('BaseGenericPlugin', 'Password', 'Token', - ] + ) diff --git a/keystoneclient/auth/identity/generic/base.py b/keystoneclient/auth/identity/generic/base.py index 0b87d06d0..1cf3e0a9a 100644 --- a/keystoneclient/auth/identity/generic/base.py +++ b/keystoneclient/auth/identity/generic/base.py @@ -12,15 +12,14 @@ import abc import logging +import urllib.parse as urlparse from oslo_config import cfg -import six -import six.moves.urllib.parse as urlparse from keystoneclient import _discover from keystoneclient.auth.identity import base from keystoneclient import exceptions -from keystoneclient.i18n import _, _LW +from keystoneclient.i18n import _ LOG = logging.getLogger(__name__) @@ -42,11 +41,10 @@ def get_options(): ] -@six.add_metaclass(abc.ABCMeta) -class BaseGenericPlugin(base.BaseIdentityPlugin): - """An identity plugin that is not version dependant. +class BaseGenericPlugin(base.BaseIdentityPlugin, metaclass=abc.ABCMeta): + """An identity plugin that is not version dependent. - Internally we will construct a version dependant plugin with the resolved + 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. """ @@ -72,9 +70,19 @@ def __init__(self, auth_url, 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 paramters. + """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 @@ -89,7 +97,7 @@ def create_plugin(self, session, version, url, raw_status=None): :returns: A plugin that can match the parameters or None if nothing. """ - return None + return None # pragma: no cover @property def _has_domain_scope(self): @@ -104,14 +112,14 @@ def _has_domain_scope(self): @property def _v2_params(self): - """Parameters that are common to v2 plugins.""" + """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): - """Parameters that are common to v3 plugins.""" + """Return parameters that are common to v3 plugins.""" return {'trust_id': self._trust_id, 'project_id': self._project_id, 'project_name': self._project_name, @@ -130,9 +138,9 @@ def _do_create_plugin(self, session): except (exceptions.DiscoveryFailure, exceptions.HTTPError, exceptions.ConnectionError): - LOG.warning(_LW('Discovering versions from the identity service ' - 'failed when creating the password plugin. ' - 'Attempting to determine version from URL.')) + 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() diff --git a/keystoneclient/auth/identity/generic/cli.py b/keystoneclient/auth/identity/generic/cli.py index c4938503d..de1d74895 100644 --- a/keystoneclient/auth/identity/generic/cli.py +++ b/keystoneclient/auth/identity/generic/cli.py @@ -15,7 +15,6 @@ from keystoneclient.auth.identity.generic import password from keystoneclient import exceptions as exc from keystoneclient.i18n import _ -from keystoneclient import utils class DefaultCLI(password.Password): @@ -25,7 +24,6 @@ class DefaultCLI(password.Password): as well as allowing users to override with a custom token and endpoint. """ - @utils.positional() def __init__(self, endpoint=None, token=None, **kwargs): super(DefaultCLI, self).__init__(**kwargs) @@ -38,6 +36,7 @@ def get_options(cls): 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 diff --git a/keystoneclient/auth/identity/generic/password.py b/keystoneclient/auth/identity/generic/password.py index f1c8aa66b..ddcdba8e3 100644 --- a/keystoneclient/auth/identity/generic/password.py +++ b/keystoneclient/auth/identity/generic/password.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import logging - from oslo_config import cfg from keystoneclient import _discover @@ -20,17 +18,15 @@ from keystoneclient.auth.identity import v3 from keystoneclient import utils -LOG = logging.getLogger(__name__) - def get_options(): return [ cfg.StrOpt('user-id', help='User id'), - cfg.StrOpt('user-name', dest='username', help='Username', - deprecated_name='username'), + 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', help="User's password"), + cfg.StrOpt('password', secret=True, help="User's password"), ] @@ -45,7 +41,6 @@ class Password(base.BaseGenericPlugin): """ - @utils.positional() def __init__(self, auth_url, username=None, user_id=None, password=None, user_domain_id=None, user_domain_name=None, **kwargs): super(Password, self).__init__(auth_url=auth_url, **kwargs) diff --git a/keystoneclient/auth/identity/generic/token.py b/keystoneclient/auth/identity/generic/token.py index 0fbacf042..e3d01aa0f 100644 --- a/keystoneclient/auth/identity/generic/token.py +++ b/keystoneclient/auth/identity/generic/token.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import logging - from oslo_config import cfg from keystoneclient import _discover @@ -19,12 +17,10 @@ from keystoneclient.auth.identity import v2 from keystoneclient.auth.identity import v3 -LOG = logging.getLogger(__name__) - def get_options(): return [ - cfg.StrOpt('token', help='Token to authenticate with'), + cfg.StrOpt('token', secret=True, help='Token to authenticate with'), ] diff --git a/keystoneclient/auth/identity/v2.py b/keystoneclient/auth/identity/v2.py index 3ea74b779..b2ecb4b59 100644 --- a/keystoneclient/auth/identity/v2.py +++ b/keystoneclient/auth/identity/v2.py @@ -14,7 +14,6 @@ import logging from oslo_config import cfg -import six from keystoneclient import access from keystoneclient.auth.identity import base @@ -24,8 +23,7 @@ _logger = logging.getLogger(__name__) -@six.add_metaclass(abc.ABCMeta) -class Auth(base.BaseIdentityPlugin): +class Auth(base.BaseIdentityPlugin, metaclass=abc.ABCMeta): """Identity V2 Authentication Plugin. :param string auth_url: Identity service endpoint for authorization. @@ -48,7 +46,6 @@ def get_options(cls): return options - @utils.positional() def __init__(self, auth_url, trust_id=None, tenant_id=None, @@ -57,10 +54,20 @@ def __init__(self, auth_url, super(Auth, self).__init__(auth_url=auth_url, reauthenticate=reauthenticate) - self.trust_id = trust_id + 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' @@ -93,6 +100,7 @@ def get_auth_data(self, headers=None): :return: A dict of authentication data for the auth type. :rtype: dict """ + pass # pragma: no cover _NOT_PASSED = object() @@ -116,7 +124,6 @@ class Password(Auth): :raises TypeError: if a user_id or username is not provided. """ - @utils.positional(4) def __init__(self, auth_url, username=_NOT_PASSED, password=None, user_id=_NOT_PASSED, **kwargs): super(Password, self).__init__(auth_url, **kwargs) @@ -131,8 +138,28 @@ def __init__(self, auth_url, username=_NOT_PASSED, password=None, user_id = None self.user_id = user_id - self.username = username - self.password = password + 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} @@ -157,9 +184,9 @@ def get_options(cls): options = super(Password, cls).get_options() options.extend([ - cfg.StrOpt('user-name', + cfg.StrOpt('username', dest='username', - deprecated_name='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'), @@ -182,7 +209,17 @@ class Token(Auth): def __init__(self, auth_url, token, **kwargs): super(Token, self).__init__(auth_url, **kwargs) - self.token = token + 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: diff --git a/keystoneclient/auth/identity/v3/__init__.py b/keystoneclient/auth/identity/v3/__init__.py index a08f3eccc..abbaa658d 100644 --- a/keystoneclient/auth/identity/v3/__init__.py +++ b/keystoneclient/auth/identity/v3/__init__.py @@ -10,13 +10,15 @@ # 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', +__all__ = ('Auth', 'AuthConstructor', 'AuthMethod', 'BaseAuth', @@ -27,4 +29,4 @@ 'PasswordMethod', 'Token', - 'TokenMethod'] + 'TokenMethod') diff --git a/keystoneclient/auth/identity/v3/base.py b/keystoneclient/auth/identity/v3/base.py index 9d1f56274..c055d4ff7 100644 --- a/keystoneclient/auth/identity/v3/base.py +++ b/keystoneclient/auth/identity/v3/base.py @@ -14,25 +14,23 @@ import logging from oslo_config import cfg -import six +from oslo_serialization import jsonutils from keystoneclient import access from keystoneclient.auth.identity import base from keystoneclient import exceptions from keystoneclient.i18n import _ -from keystoneclient import utils _logger = logging.getLogger(__name__) -__all__ = ['Auth', 'AuthMethod', 'AuthConstructor', 'BaseAuth'] +__all__ = ('Auth', 'AuthMethod', 'AuthConstructor', 'BaseAuth') -@six.add_metaclass(abc.ABCMeta) -class BaseAuth(base.BaseIdentityPlugin): +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 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. @@ -46,7 +44,6 @@ class BaseAuth(base.BaseIdentityPlugin): token. (optional) default True. """ - @utils.positional() def __init__(self, auth_url, trust_id=None, domain_id=None, @@ -59,7 +56,7 @@ def __init__(self, auth_url, include_catalog=True): super(BaseAuth, self).__init__(auth_url=auth_url, reauthenticate=reauthenticate) - self.trust_id = trust_id + self._trust_id = trust_id self.domain_id = domain_id self.domain_name = domain_name self.project_id = project_id @@ -68,6 +65,16 @@ def __init__(self, auth_url, 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.""" @@ -75,7 +82,7 @@ def token_url(self): @abc.abstractmethod def get_auth_ref(self, session, **kwargs): - return None + return None # pragma: no cover @classmethod def get_options(cls): @@ -100,7 +107,7 @@ 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 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. @@ -180,6 +187,7 @@ def get_auth_ref(self, session, **kwargs): 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) @@ -188,8 +196,7 @@ def get_auth_ref(self, session, **kwargs): **resp_data) -@six.add_metaclass(abc.ABCMeta) -class AuthMethod(object): +class AuthMethod(object, metaclass=abc.ABCMeta): """One part of a V3 Authentication strategy. V3 Tokens allow multiple methods to be presented when authentication @@ -208,7 +215,7 @@ def __init__(self, **kwargs): setattr(self, param, kwargs.pop(param, None)) if kwargs: - msg = _("Unexpected Attributes: %s") % ", ".join(kwargs.keys()) + msg = _("Unexpected Attributes: %s") % ", ".join(kwargs) raise AttributeError(msg) @classmethod @@ -223,19 +230,21 @@ def get_auth_data(self, session, auth, headers, **kwargs): :param session: The communication session. :type session: keystoneclient.session.Session - :param Auth auth: The auth plugin calling the method. + :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. -@six.add_metaclass(abc.ABCMeta) -class AuthConstructor(Auth): - """AuthConstructor is a means of creating an Auth Plugin that contains - only one authentication method. This is generally the required usage. + 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 diff --git a/keystoneclient/auth/identity/v3/federated.py b/keystoneclient/auth/identity/v3/federated.py index f6416eb87..755e7f51b 100644 --- a/keystoneclient/auth/identity/v3/federated.py +++ b/keystoneclient/auth/identity/v3/federated.py @@ -13,21 +13,21 @@ import abc from oslo_config import cfg -import six from keystoneclient.auth.identity.v3 import base from keystoneclient.auth.identity.v3 import token -__all__ = ['FederatedBaseAuth'] +__all__ = ('FederatedBaseAuth',) -@six.add_metaclass(abc.ABCMeta) -class FederatedBaseAuth(base.BaseAuth): +class FederatedBaseAuth(base.BaseAuth, metaclass=abc.ABCMeta): rescoping_plugin = token.Token def __init__(self, auth_url, identity_provider, protocol, **kwargs): - """Class constructor accepting following parameters: + """Class constructor for federated authentication plugins. + + Accepting following parameters: :param auth_url: URL of the Identity Service :type auth_url: string @@ -116,3 +116,4 @@ def get_auth_ref(self, session, **kwargs): @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 index d9cfa4a14..99094a1e2 100644 --- a/keystoneclient/auth/identity/v3/password.py +++ b/keystoneclient/auth/identity/v3/password.py @@ -16,7 +16,7 @@ from keystoneclient import utils -__all__ = ['PasswordMethod', 'Password'] +__all__ = ('PasswordMethod', 'Password') class PasswordMethod(base.AuthMethod): @@ -79,8 +79,8 @@ def get_options(cls): options.extend([ cfg.StrOpt('user-id', help='User ID'), - cfg.StrOpt('user-name', dest='username', help='Username', - deprecated_name='username'), + 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"), diff --git a/keystoneclient/auth/identity/v3/token.py b/keystoneclient/auth/identity/v3/token.py index d92d3fcce..396a11a20 100644 --- a/keystoneclient/auth/identity/v3/token.py +++ b/keystoneclient/auth/identity/v3/token.py @@ -15,7 +15,7 @@ from keystoneclient.auth.identity.v3 import base -__all__ = ['TokenMethod', 'Token'] +__all__ = ('TokenMethod', 'Token') class TokenMethod(base.AuthMethod): diff --git a/keystoneclient/auth/token_endpoint.py b/keystoneclient/auth/token_endpoint.py index 405506290..6b60f9cb8 100644 --- a/keystoneclient/auth/token_endpoint.py +++ b/keystoneclient/auth/token_endpoint.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings + from oslo_config import cfg from keystoneclient.auth import base @@ -25,6 +27,12 @@ class Token(base.BaseAuthPlugin): 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 diff --git a/keystoneclient/base.py b/keystoneclient/base.py index 025362bf9..4f3ed227a 100644 --- a/keystoneclient/base.py +++ b/keystoneclient/base.py @@ -15,20 +15,37 @@ # 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 -import six -from six.moves import urllib +from keystoneauth1 import exceptions as ksa_exceptions +from keystoneauth1 import plugin +from oslo_utils import strutils -from keystoneclient import auth -from keystoneclient import exceptions +from keystoneclient import exceptions as ksc_exceptions from keystoneclient.i18n import _ -from keystoneclient.openstack.common.apiclient import base + + +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): @@ -37,27 +54,22 @@ def getid(obj): Abstracts the common pattern of allowing both an object or an object's ID (UUID) as a parameter when dealing with relationships. """ - try: - if obj.uuid: - return obj.uuid - except AttributeError: - pass - try: - return obj.id - except AttributeError: - return obj + 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 six.iteritems(kwargs) if v is not 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 six.iteritems(kwargs): + for key, ref in kwargs.items(): if ref is None: # drop null values continue @@ -83,6 +95,7 @@ class Manager(object): :param client: instance of BaseClient descendant for HTTP requests """ + resource_class = None def __init__(self, client): @@ -91,10 +104,24 @@ def __init__(self, client): @property def api(self): - """Deprecated. Use `client` instead. + """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. @@ -120,10 +147,13 @@ def _list(self, url, response_key, obj_class=None, body=None, **kwargs): # unlike other services which just return the list... try: data = data['values'] - except (KeyError, TypeError): + 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 - return [obj_class(self, res, loaded=True) for res in data if res] + return self._prepare_return_value( + resp, [obj_class(self, res, loaded=True) for res in data if res]) def _get(self, url, response_key, **kwargs): """Get an object from collection. @@ -134,7 +164,8 @@ def _get(self, url, response_key, **kwargs): :param kwargs: Additional arguments will be passed to the request. """ resp, body = self.client.get(url, **kwargs) - return self.resource_class(self, body[response_key], loaded=True) + 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. @@ -143,12 +174,7 @@ def _head(self, url, **kwargs): :param kwargs: Additional arguments will be passed to the request. """ resp, body = self.client.head(url, **kwargs) - return resp.status_code == 204 - - def _create(self, url, body, response_key, return_raw=False, **kwargs): - """Deprecated. Use `_post` instead. - """ - return self._post(url, body, response_key, return_raw, **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. @@ -165,7 +191,8 @@ def _post(self, url, body, response_key, return_raw=False, **kwargs): 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. @@ -181,9 +208,15 @@ def _put(self, url, body=None, response_key=None, **kwargs): # PUT requests may not return a body if body is not None: if response_key is not None: - return self.resource_class(self, body[response_key]) + return self._prepare_return_value( + resp, self.resource_class(self, body[response_key])) else: - return self.resource_class(self, body) + 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. @@ -197,9 +230,11 @@ def _patch(self, url, body=None, response_key=None, **kwargs): """ resp, body = self.client.patch(url, body=body, **kwargs) if response_key is not None: - return self.resource_class(self, body[response_key]) + return self._prepare_return_value( + resp, self.resource_class(self, body[response_key])) else: - return self.resource_class(self, body) + return self._prepare_return_value( + resp, self.resource_class(self, body)) def _delete(self, url, **kwargs): """Delete an object. @@ -207,7 +242,8 @@ def _delete(self, url, **kwargs): :param url: a partial URL, e.g., '/servers/my-server' :param kwargs: Additional arguments will be passed to the request. """ - return self.client.delete(url, **kwargs) + 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", **kwargs): @@ -218,20 +254,22 @@ def _update(self, url, body=None, response_key=None, method="PUT", 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) -@six.add_metaclass(abc.ABCMeta) -class ManagerWithFind(Manager): +class ManagerWithFind(Manager, metaclass=abc.ABCMeta): """Manager with additional `find()`/`findall()` methods.""" @abc.abstractmethod def list(self): - pass + pass # pragma: no cover def find(self, **kwargs): """Find a single item with attributes matching ``**kwargs``. @@ -240,16 +278,20 @@ def find(self, **kwargs): the Python side. """ rl = self.findall(**kwargs) - num = len(rl) - if num == 0: + 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 exceptions.NotFound(404, msg) - elif num > 1: - raise exceptions.NoUniqueMatch + raise ksa_exceptions.NotFound(404, msg) + elif len(rl) > 1: + raise ksc_exceptions.NoUniqueMatch else: - return rl[0] + return base_response if self.client.include_metadata else rl[0] def findall(self, **kwargs): """Find all items with attributes matching ``**kwargs``. @@ -260,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): @@ -284,12 +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, dict_args_in_out=None): - """Builds a resource URL for the given kwargs. + """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. @@ -327,7 +378,7 @@ def build_url(self, dict_args_in_out=None): @filter_kwargs def create(self, **kwargs): url = self.build_url(dict_args_in_out=kwargs) - return self._create( + return self._post( url, {self.key: kwargs}, self.key) @@ -343,10 +394,20 @@ def head(self, **kwargs): return self._head(self.build_url(dict_args_in_out=kwargs)) def _build_query(self, params): - return '?%s' % urllib.parse.urlencode(params) if params else '' + 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): - """Builds a query that does not include values, just keys. + """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 @@ -356,20 +417,42 @@ def build_key_only_query(self, params_list): @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) try: query = self._build_query(kwargs) url_query = '%(url)s%(query)s' % {'url': url, 'query': query} - return self._list( - url_query, - self.collection_key) - except exceptions.EmptyCatalog: + 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: - return self._list( - url_query, - self.collection_key, - endpoint_filter={'interface': auth.AUTH_INTERFACE}) + 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 @@ -400,29 +483,132 @@ def find(self, **kwargs): url = self.build_url(dict_args_in_out=kwargs) query = self._build_query(kwargs) - rl = self._list( - '%(url)s%(query)s' % { - 'url': url, - 'query': query, - }, + url_query = '%(url)s%(query)s' % { + 'url': url, + 'query': query + } + elements = self._list( + url_query, self.collection_key) - num = len(rl) - if num == 0: + 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 exceptions.NotFound(404, msg) - elif num > 1: - raise exceptions.NoUniqueMatch + raise ksa_exceptions.NotFound(404, msg) + elif len(elements) > 1: + raise ksc_exceptions.NoUniqueMatch else: - return rl[0] + return (base_response if self.client.include_metadata + else elements[0]) -class Resource(base.Resource): +class Resource(object): """Base class for OpenStack resources (tenant, user, etc.). 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.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 + if not self.is_loaded(): + self.get() + return self.__getattr__(k) + + raise AttributeError(k) + else: + return self.__dict__[k] + + 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'): + return + + new = self.manager.get(self.id) + if new: + self._add_details(new._info) + + 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 + 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 index 97943684d..ca39f6d14 100644 --- a/keystoneclient/baseclient.py +++ b/keystoneclient/baseclient.py @@ -10,10 +10,17 @@ # 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): diff --git a/keystoneclient/client.py b/keystoneclient/client.py index f4b9f87fe..b2dcf0628 100644 --- a/keystoneclient/client.py +++ b/keystoneclient/client.py @@ -11,14 +11,9 @@ # under the License. from keystoneclient import discover -from keystoneclient import httpclient from keystoneclient import session as client_session -# Using client.HTTPClient is deprecated. Use httpclient.HTTPClient instead. -HTTPClient = httpclient.HTTPClient - - def Client(version=None, unstable=False, session=None, **kwargs): """Factory function to create a new identity service client. @@ -30,7 +25,7 @@ def Client(version=None, unstable=False, session=None, **kwargs): 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)``. + 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 @@ -47,7 +42,7 @@ def Client(version=None, unstable=False, session=None, **kwargs): cannot be found. """ if not session: - session = client_session.Session.construct(kwargs) + 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 68af1dd1e..2ee8b52ae 100644 --- a/keystoneclient/common/cms.py +++ b/keystoneclient/common/cms.py @@ -23,13 +23,12 @@ import errno import hashlib import logging -import textwrap import zlib -import six +from debtcollector import removals from keystoneclient import exceptions -from keystoneclient.i18n import _, _LE, _LW +from keystoneclient.i18n import _ subprocess = None @@ -38,13 +37,15 @@ PKIZ_PREFIX = 'PKIZ_' PKIZ_CMS_FORM = 'DER' PKI_ASN1_FORM = 'PEM' -DEFAULT_TOKEN_DIGEST_ALGORITHM = 'sha256' +# 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/apps/cms.html#EXIT_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 @@ -60,13 +61,20 @@ def _ensure_subprocess(): 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 # noqa + 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 set_subprocess(_subprocess=None): """Set subprocess module to use. + The subprocess could be eventlet.green.subprocess if using eventlet, or Python's subprocess otherwise. """ @@ -84,9 +92,7 @@ def _check_files_accessible(files): except IOError as e: # Catching IOError means there is an issue with # the given file. - err = _('Hit OSError in _process_communicate_handle_oserror()\n' - 'Likely due to %(file)s: %(error)s') % {'file': try_file, - 'error': e.strerror} + 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 @@ -96,13 +102,12 @@ def _check_files_accessible(files): 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 Python 2.6.x/old 2.7.x + # 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 @@ -110,12 +115,25 @@ def _process_communicate_handle_oserror(process, data, files): retcode, err = _check_files_accessible(files) if process.stderr: msg = process.stderr.read() - err = err + msg.decode('utf-8') + 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: - err = err.decode('utf-8') + if isinstance(err, bytes): + err = err.decode('utf-8') return output, err, retcode @@ -135,7 +153,7 @@ def _encoding_for_form(inform): def cms_verify(formatted, signing_cert_file_name, ca_file_name, inform=PKI_ASN1_FORM): - """Verifies the signature of the contents IAW CMS syntax. + """Verify the signature of the contents IAW CMS syntax. :raises subprocess.CalledProcessError: :raises keystoneclient.exceptions.CertificateConfigError: if certificate @@ -143,8 +161,8 @@ def cms_verify(formatted, signing_cert_file_name, ca_file_name, properly. """ _ensure_subprocess() - if isinstance(formatted, six.string_types): - data = bytearray(formatted, _encoding_for_form(inform)) + if isinstance(formatted, str): + data = bytes(formatted, _encoding_for_form(inform)) else: data = formatted process = subprocess.Popen(['openssl', 'cms', '-verify', @@ -163,32 +181,38 @@ def cms_verify(formatted, 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 with not exist file, return code 2 - # and error msg will be returned. + # 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 - # http://www.openssl.org/docs/apps/cms.html#EXIT_CODES + # 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: - # NOTE(dmllr): Python 2.6 compatibility: - # CalledProcessError did not have output keyword argument - e = subprocess.CalledProcessError(retcode, 'openssl') - e.output = err - raise e + raise subprocess.CalledProcessError(retcode, 'openssl', output=err) return output def is_pkiz(token_text): - """Determine if a token a cmsz token + """Determine if a token is PKIZ. Checks if the string has the prefix that indicates it is a Crypto Message Syntax, Z compressed token. @@ -226,17 +250,15 @@ def pkiz_verify(signed_text, signing_cert_file_name, ca_file_name): inform=PKIZ_CMS_FORM) -# This function is deprecated and will be removed once the ASN1 token format -# is no longer required. It is only here to be used for testing. def token_to_cms(signed_text): - """Converts a custom formatted token to a PEM-formatted token. + """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 += textwrap.wrap(copy_of_text, 64) + 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) @@ -297,10 +319,14 @@ def is_asn1_token(token): 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. Use is_asn1_token() instead.""" - LOG.warning(_LW('The function is_ans1_token() is deprecated, ' - 'use is_asn1_token() instead.')) + """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) @@ -313,7 +339,7 @@ def cms_sign_text(data_to_sign, signing_cert_file_name, signing_key_file_name, 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): - """Uses OpenSSL to sign a document. + """Use OpenSSL to sign a document. Produces a Base64 encoding of a DER formatted CMS Document http://en.wikipedia.org/wiki/Cryptographic_Message_Syntax @@ -329,8 +355,8 @@ def cms_sign_data(data_to_sign, signing_cert_file_name, signing_key_file_name, """ _ensure_subprocess() - if isinstance(data_to_sign, six.string_types): - data = bytearray(data_to_sign, encoding='utf-8') + 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', @@ -350,11 +376,11 @@ def cms_sign_data(data_to_sign, 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(_LE('Signing error: Unable to load certificate - ' - 'ensure you have configured PKI with ' - '"keystone-manage pki_setup"')) + LOG.error('Signing error: Unable to load certificate - ' + 'ensure you have configured PKI with ' + '"keystone-manage pki_setup"') else: - LOG.error(_LE('Signing error: %s'), err) + LOG.error('Signing error: %s', err) raise subprocess.CalledProcessError(retcode, 'openssl') if outform == PKI_ASN1_FORM: return output.decode('utf-8') @@ -370,7 +396,7 @@ def cms_sign_token(text, signing_cert_file_name, signing_key_file_name, def cms_to_token(cms_text): - """Converts a CMS-signed token in PEM format to a custom URL-safe format. + """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 @@ -410,7 +436,7 @@ def cms_hash_token(token_id, mode='md5'): return None if is_asn1_token(token_id) or is_pkiz(token_id): hasher = hashlib.new(mode) - if isinstance(token_id, six.text_type): + if isinstance(token_id, str): token_id = token_id.encode('utf-8') hasher.update(token_id) return hasher.hexdigest() 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 index f3bb1052c..acf3f0513 100644 --- a/keystoneclient/contrib/auth/v3/saml2.py +++ b/keystoneclient/contrib/auth/v3/saml2.py @@ -11,11 +11,11 @@ # under the License. import datetime +import urllib.parse import uuid -from lxml import etree +from lxml import etree # nosec(cjschaef): used to create xml, not parse it from oslo_config import cfg -from six.moves import urllib from keystoneclient import access from keystoneclient.auth.identity import v3 @@ -26,6 +26,8 @@ class _BaseSAMLPlugin(v3.AuthConstructor): HTTP_MOVED_TEMPORARILY = 302 + HTTP_SEE_OTHER = 303 + PROTOCOL = 'saml2' @staticmethod @@ -70,9 +72,9 @@ def get_options(cls): cfg.StrOpt('identity-provider', help="Identity Provider's name"), cfg.StrOpt('identity-provider-url', help="Identity Provider's URL"), - cfg.StrOpt('user-name', dest='username', help='Username', - deprecated_name='username'), - cfg.StrOpt('password', help='Password') + cfg.StrOpt('username', dest='username', help='Username', + deprecated_name='user-name'), + cfg.StrOpt('password', secret=True, help='Password') ]) return options @@ -86,7 +88,7 @@ def get_auth_data(self, session, auth, headers, **kwargs): class Saml2UnscopedToken(_BaseSAMLPlugin): - """Implement authentication plugin for SAML2 protocol. + 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 @@ -126,7 +128,7 @@ class Saml2UnscopedToken(_BaseSAMLPlugin): SAML2_HEADER_INDEX = 0 ECP_SP_EMPTY_REQUEST_HEADERS = { - 'Accept': 'text/html; application/vnd.paos+xml', + 'Accept': 'text/html, application/vnd.paos+xml', 'PAOS': ('ver="urn:liberty:paos:2003-08";"urn:oasis:names:tc:' 'SAML:2.0:profiles:SSO:ecp"') } @@ -170,11 +172,31 @@ def __init__(self, auth_url, 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 + 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 - def _handle_http_302_ecp_redirect(self, session, response, method, - **kwargs): - if response.status_code != self.HTTP_MOVED_TEMPORARILY: + @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'] @@ -271,7 +293,6 @@ def _send_service_provider_request(self, session): 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 @@ -306,8 +327,8 @@ def _send_service_provider_saml2_authn_response(self, session): authenticated user. This function directs the HTTP request to SP managed URL, for instance: ``https://:/Shibboleth.sso/ SAML2/ECP``. - Upon success the there's a session created and access to the protected - resource is granted. Many implementations of the SP return HTTP 302 + 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 @@ -324,11 +345,11 @@ def _send_service_provider_saml2_authn_response(self, session): data=etree.tostring(self.saml2_idp_authn_response), authenticated=False, redirect=False) - # Don't follow HTTP specs - after the HTTP 302 response don't repeat - # the call directed to the Location URL. In this case, this is an - # indication that saml2 session is now active and protected resource + # 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_302_ecp_redirect( + response = self._handle_http_ecp_redirect( session, response, method='GET', headers=self.ECP_SP_SAML2_REQUEST_HEADERS) @@ -490,7 +511,27 @@ def __init__(self, auth_url, identity_provider, identity_provider_url, 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 + 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): @@ -517,7 +558,8 @@ def _cookies(self, session): """ try: return bool(session.cookies) - except AttributeError: + except AttributeError: # nosec(cjschaef): fetch cookies from + # underylying requests.Session object, or fail trying pass return bool(session.session.cookies) @@ -538,7 +580,6 @@ def _token_dates(self, fmt='%Y-%m-%dT%H:%M:%S.%fZ'): :type fmt: string """ - date_created = datetime.datetime.utcnow() date_expires = date_created + datetime.timedelta( seconds=self.DEFAULT_ADFS_TOKEN_EXPIRATION) @@ -550,7 +591,6 @@ def _prepare_adfs_request(self): 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') @@ -809,7 +849,7 @@ def _get_unscoped_token(self, session, *kwargs): This is a multistep process:: * Prepare ADFS Request Securty Token - - build a etree.XML object filling certain attributes with proper user + 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. @@ -862,7 +902,6 @@ class Saml2ScopedTokenMethod(v3.TokenMethod): 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 diff --git a/keystoneclient/contrib/bootstrap/shell.py b/keystoneclient/contrib/bootstrap/shell.py deleted file mode 100644 index 9a4ed9fcf..000000000 --- a/keystoneclient/contrib/bootstrap/shell.py +++ /dev/null @@ -1,40 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -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 95e4bb188..f7cefa1bf 100644 --- a/keystoneclient/contrib/ec2/utils.py +++ b/keystoneclient/contrib/ec2/utils.py @@ -20,16 +20,16 @@ import hashlib import hmac import re - -import six -from six.moves import urllib +import urllib.parse from keystoneclient.i18n import _ class Ec2Signer(object): - """Utility class which allows a request to be signed with an AWS style - signature, which can then be used for authentication via the keystone ec2 + """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. """ @@ -40,8 +40,10 @@ def __init__(self, secret_key): 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, since AWS - removed the SignatureVersion field from the v4 request spec... + """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 @@ -67,7 +69,8 @@ def _v4_creds(self, credentials): if (credentials['params']['X-Amz-Algorithm'] == 'AWS4-HMAC-SHA256'): return True - except KeyError: + except KeyError: # nosec(cjschaef): in cases of not finding + # entries, simply return False pass return False @@ -101,9 +104,9 @@ def generate(self, credentials): @staticmethod def _get_utf8_value(value): """Get the UTF8-encoded version of a value.""" - if not isinstance(value, (six.binary_type, six.text_type)): + if not isinstance(value, (str, bytes)): value = str(value) - if isinstance(value, six.text_type): + if isinstance(value, str): return value.encode('utf-8') else: return value @@ -116,9 +119,7 @@ def _calc_signature_0(self, params): def _calc_signature_1(self, params): """Generate AWS signature version 1 string.""" - keys = list(params) - keys.sort(key=six.text_type.lower) - for key in keys: + 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) @@ -126,8 +127,9 @@ def _calc_signature_1(self, params): @staticmethod def _canonical_qs(params): - """Construct a sorted, correctly encoded query string as required for - _calc_signature_2 and _calc_signature_4. + """Construct a sorted, correctly encoded query string. + + This is required for _calc_signature_2 and _calc_signature_4. """ keys = list(params) keys.sort() @@ -156,14 +158,14 @@ def _calc_signature_2(self, params, verb, server_string, path): 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 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), @@ -189,8 +191,9 @@ def auth_param(param_name): return param_str def date_param(): - """Get the X-Amz-Date' value, which can be either a header - or parameter. + """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 @@ -211,14 +214,14 @@ def canonical_header_str(): # - the Authorization header (SignedHeaders key) # - the X-Amz-SignedHeaders query parameter headers_lower = dict((k.lower().strip(), v.strip()) - for (k, v) in six.iteritems(headers)) + 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('Boto/2.[0-9].[0-2]', user_agent) + strip_port = re.match(r'Boto/2\.[0-9]\.[0-2]', user_agent) header_list = [] sh_str = auth_param('SignedHeaders') diff --git a/keystoneclient/contrib/revoke/model.py b/keystoneclient/contrib/revoke/model.py deleted file mode 100644 index bb62840c2..000000000 --- a/keystoneclient/contrib/revoke/model.py +++ /dev/null @@ -1,314 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from oslo_utils import timeutils - -# The set of attributes common between the RevokeEvent -# and the dictionaries created from the token Data. -_NAMES = ['trust_id', - 'consumer_id', - 'access_token_id', - 'expires_at', - 'domain_id', - 'project_id', - 'user_id', - 'role_id'] - - -# Additional arguments for creating a RevokeEvent -_EVENT_ARGS = ['issued_before', 'revoked_at'] - -# Values that will be in the token data but not in the event. -# These will compared with event values that have different names. -# For example: both trustor_id and trustee_id are compared against user_id -_TOKEN_KEYS = ['identity_domain_id', - 'assignment_domain_id', - 'issued_at', - 'trustor_id', - 'trustee_id'] - - -REVOKE_KEYS = _NAMES + _EVENT_ARGS - - -def blank_token_data(issued_at): - token_data = dict() - for name in _NAMES: - token_data[name] = None - for name in _TOKEN_KEYS: - token_data[name] = None - # required field - token_data['issued_at'] = issued_at - return token_data - - -class RevokeEvent(object): - def __init__(self, **kwargs): - for k in REVOKE_KEYS: - v = kwargs.get(k, None) - setattr(self, k, v) - if self.revoked_at is None: - self.revoked_at = timeutils.utcnow() - if self.issued_before is None: - self.issued_before = self.revoked_at - - def to_dict(self): - keys = ['user_id', - 'role_id', - 'domain_id', - 'project_id'] - event = dict((key, self.__dict__[key]) for key in keys - if self.__dict__[key] is not None) - if self.trust_id is not None: - event['OS-TRUST:trust_id'] = self.trust_id - if self.consumer_id is not None: - event['OS-OAUTH1:consumer_id'] = self.consumer_id - if self.consumer_id is not None: - event['OS-OAUTH1:access_token_id'] = self.access_token_id - if self.expires_at is not None: - event['expires_at'] = timeutils.isotime(self.expires_at, - subsecond=True) - if self.issued_before is not None: - event['issued_before'] = timeutils.isotime(self.issued_before, - subsecond=True) - return event - - def key_for_name(self, name): - return "%s=%s" % (name, getattr(self, name) or '*') - - -def attr_keys(event): - return map(event.key_for_name, _NAMES) - - -class RevokeTree(object): - """Fast Revocation Checking Tree Structure - - The Tree is an index to quickly match tokens against events. - Each node is a hashtable of key=value combinations from revocation events. - The - - """ - - def __init__(self, revoke_events=None): - self.revoke_map = dict() - self.add_events(revoke_events) - - def add_event(self, event): - """Updates the tree based on a revocation event. - - Creates any necessary internal nodes in the tree corresponding to the - fields of the revocation event. The leaf node will always be set to - the latest 'issued_before' for events that are otherwise identical. - - :param: Event to add to the tree - - :returns: the event that was passed in. - - """ - revoke_map = self.revoke_map - for key in attr_keys(event): - revoke_map = revoke_map.setdefault(key, {}) - revoke_map['issued_before'] = max( - event.issued_before, revoke_map.get( - 'issued_before', event.issued_before)) - return event - - def remove_event(self, event): - """Update the tree based on the removal of a Revocation Event - - Removes empty nodes from the tree from the leaf back to the root. - - If multiple events trace the same path, but have different - 'issued_before' values, only the last is ever stored in the tree. - So only an exact match on 'issued_before' ever triggers a removal - - :param: Event to remove from the tree - - """ - stack = [] - revoke_map = self.revoke_map - for name in _NAMES: - key = event.key_for_name(name) - nxt = revoke_map.get(key) - if nxt is None: - break - stack.append((revoke_map, key, nxt)) - revoke_map = nxt - else: - if event.issued_before == revoke_map['issued_before']: - revoke_map.pop('issued_before') - for parent, key, child in reversed(stack): - if not any(child): - del parent[key] - - def add_events(self, revoke_events): - return map(self.add_event, revoke_events or []) - - def is_revoked(self, token_data): - """Check if a token matches the revocation event - - Compare the values for each level of the tree with the values from - the token, accounting for attributes that have alternative - keys, and for wildcard matches. - if there is a match, continue down the tree. - if there is no match, exit early. - - token_data is a map based on a flattened view of token. - The required fields are: - - 'expires_at','user_id', 'project_id', 'identity_domain_id', - 'assignment_domain_id', 'trust_id', 'trustor_id', 'trustee_id' - 'consumer_id', 'access_token_id' - - """ - # Alternative names to be checked in token for every field in - # revoke tree. - alternatives = { - 'user_id': ['user_id', 'trustor_id', 'trustee_id'], - 'domain_id': ['identity_domain_id', 'assignment_domain_id'], - } - # Contains current forest (collection of trees) to be checked. - partial_matches = [self.revoke_map] - # We iterate over every layer of our revoke tree (except the last one). - for name in _NAMES: - # bundle is the set of partial matches for the next level down - # the tree - bundle = [] - wildcard = '%s=*' % (name,) - # For every tree in current forest. - for tree in partial_matches: - # If there is wildcard node on current level we take it. - bundle.append(tree.get(wildcard)) - if name == 'role_id': - # Roles are very special since a token has a list of them. - # If the revocation event matches any one of them, - # revoke the token. - for role_id in token_data.get('roles', []): - bundle.append(tree.get('role_id=%s' % role_id)) - else: - # For other fields we try to get any branch that concur - # with any alternative field in the token. - for alt_name in alternatives.get(name, [name]): - bundle.append( - tree.get('%s=%s' % (name, token_data[alt_name]))) - # tree.get returns `None` if there is no match, so `bundle.append` - # adds a 'None' entry. This call remoes the `None` entries. - partial_matches = [x for x in bundle if x is not None] - if not partial_matches: - # If we end up with no branches to follow means that the token - # is definitely not in the revoke tree and all further - # iterations will be for nothing. - return False - - # The last (leaf) level is checked in a special way because we verify - # issued_at field differently. - for leaf in partial_matches: - try: - if leaf['issued_before'] > token_data['issued_at']: - return True - except KeyError: - pass - # If we made it out of the loop then no element in revocation tree - # corresponds to our token and it is good. - return False - - -def build_token_values_v2(access, default_domain_id): - token_data = access['token'] - token_values = { - 'expires_at': timeutils.normalize_time( - timeutils.parse_isotime(token_data['expires'])), - 'issued_at': timeutils.normalize_time( - timeutils.parse_isotime(token_data['issued_at']))} - - token_values['user_id'] = access.get('user', {}).get('id') - - project = token_data.get('tenant') - if project is not None: - token_values['project_id'] = project['id'] - else: - token_values['project_id'] = None - - token_values['identity_domain_id'] = default_domain_id - token_values['assignment_domain_id'] = default_domain_id - - trust = token_data.get('trust') - if trust is None: - token_values['trust_id'] = None - token_values['trustor_id'] = None - token_values['trustee_id'] = None - else: - token_values['trust_id'] = trust['id'] - token_values['trustor_id'] = trust['trustor_id'] - token_values['trustee_id'] = trust['trustee_id'] - - token_values['consumer_id'] = None - token_values['access_token_id'] = None - - role_list = [] - # Roles are by ID in metadata and by name in the user section - roles = access.get('metadata', {}).get('roles', []) - for role in roles: - role_list.append(role) - token_values['roles'] = role_list - return token_values - - -def build_token_values(token_data): - token_values = { - 'expires_at': timeutils.normalize_time( - timeutils.parse_isotime(token_data['expires_at'])), - 'issued_at': timeutils.normalize_time( - timeutils.parse_isotime(token_data['issued_at']))} - - user = token_data.get('user') - if user is not None: - token_values['user_id'] = user['id'] - token_values['identity_domain_id'] = user['domain']['id'] - else: - token_values['user_id'] = None - token_values['identity_domain_id'] = None - - project = token_data.get('project', token_data.get('tenant')) - if project is not None: - token_values['project_id'] = project['id'] - token_values['assignment_domain_id'] = project['domain']['id'] - else: - token_values['project_id'] = None - token_values['assignment_domain_id'] = None - - role_list = [] - roles = token_data.get('roles') - if roles is not None: - for role in roles: - role_list.append(role['id']) - token_values['roles'] = role_list - - trust = token_data.get('OS-TRUST:trust') - if trust is None: - token_values['trust_id'] = None - token_values['trustor_id'] = None - token_values['trustee_id'] = None - else: - token_values['trust_id'] = trust['id'] - token_values['trustor_id'] = trust['trustor_user']['id'] - token_values['trustee_id'] = trust['trustee_user']['id'] - - oauth1 = token_data.get('OS-OAUTH1') - if oauth1 is None: - token_values['consumer_id'] = None - token_values['access_token_id'] = None - else: - token_values['consumer_id'] = oauth1['consumer_id'] - token_values['access_token_id'] = oauth1['access_token_id'] - return token_values diff --git a/keystoneclient/discover.py b/keystoneclient/discover.py index 40fd85db3..16174162d 100644 --- a/keystoneclient/discover.py +++ b/keystoneclient/discover.py @@ -10,22 +10,19 @@ # License for the specific language governing permissions and limitations # under the License. -import logging +import warnings -import six +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 import utils from keystoneclient.v2_0 import client as v2_client from keystoneclient.v3 import client as v3_client -_logger = logging.getLogger(__name__) - - _CLIENT_VERSIONS = {2: v2_client.Client, 3: v3_client.Client} @@ -53,8 +50,7 @@ def normalize_version_number(version): def version_match(required, candidate): - """Test that an available version is a suitable match for a required - version. + """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. @@ -73,14 +69,15 @@ def 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) + session = client_session.Session._construct(kwargs) return _discover.get_version_data(session, url) class Discover(_discover.Discover): - """A means to discover and create clients depending on the supported API - versions on the server. + """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. @@ -95,6 +92,12 @@ class Discover(_discover.Discover): 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 @@ -104,45 +107,51 @@ class Discover(_discover.Discover): 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) DEPRECATED: use - the session object. This is ignored if a session - is provided. + '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) - DEPRECATED: use the session object. This is ignored - if a session is provided. + 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) DEPRECATED: use the - session object. This is ignored if a session is - provided. + 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) DEPRECATED: use the session object. - This is ignored if a session is provided. + 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) DEPRECATED: use the - session object. This is ignored if a session is - provided. + 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) DEPRECATED: use the - session object. This is ignored if a session is - provided. + 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). """ - @utils.positional(2) def __init__(self, session=None, authenticated=None, **kwargs): if not session: - session = client_session.Session.construct(kwargs) + 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 @@ -155,24 +164,33 @@ def __init__(self, session=None, authenticated=None, **kwargs): 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 ' - 'auth_url or endpoint')) + _('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 and the data - associated with them. + """Return a list of identity APIs available on the server. + + The list returned includes the data associated with them. + + .. warning:: - DEPRECATED: use raw_version_data() + 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) - DEPRECTED. Equates to setting allow_experimental + 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. @@ -185,6 +203,10 @@ def available_versions(self, **kwargs): """ 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. @@ -192,15 +214,17 @@ def raw_version_data(self, unstable=False, **kwargs): 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: (deprecated) equates to setting - allow_experimental and allow_unknown. + :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 + :rtype: List Example:: @@ -208,8 +232,8 @@ def raw_version_data(self, unstable=False, **kwargs): >>> disc = discover.Discovery(auth_url='http://localhost:5000') >>> disc.raw_version_data() [{'id': 'v3.0', - 'links': [{'href': u'http://127.0.0.1:5000/v3/', - 'rel': u'self'}], + 'links': [{'href': 'http://127.0.0.1:5000/v3/', + 'rel': 'self'}], 'media-types': [ {'base': 'application/json', 'type': 'application/vnd.openstack.identity-v3+json'}, @@ -218,11 +242,11 @@ def raw_version_data(self, unstable=False, **kwargs): 'status': 'stable', 'updated': '2013-03-06T00:00:00Z'}, {'id': 'v2.0', - 'links': [{'href': u'http://127.0.0.1:5000/v2.0/', - 'rel': u'self'}, - {'href': u'...', - 'rel': u'describedby', - 'type': u'application/pdf'}], + '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'}, @@ -269,7 +293,7 @@ def _create_client(self, version_data, **kwargs): raise exceptions.DiscoveryFailure(msg) # kwargs should take priority over stored kwargs. - for k, v in six.iteritems(self._client_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 @@ -308,7 +332,7 @@ def create_client(self, version=None, unstable=False, **kwargs): def add_catalog_discover_hack(service_type, old, new): - """Adds a version removal rule for a particular service. + """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 diff --git a/keystoneclient/exceptions.py b/keystoneclient/exceptions.py index 0150bf528..034e5c977 100644 --- a/keystoneclient/exceptions.py +++ b/keystoneclient/exceptions.py @@ -12,28 +12,348 @@ # 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. -.. py:exception:: AuthorizationFailure +ConnectionError = _exc.ConnectionError +"""Cannot connect to API service. -.. py:exception:: ClientException +An alias of :py:exc:`keystoneauth1.exceptions.connection.ConnectionError` +""" -.. py:exception:: HttpError +ConnectionRefused = _exc.ConnectFailure +"""Connection refused while trying to connect to API service. -.. py:exception:: ValidationError +An alias of :py:exc:`keystoneauth1.exceptions.connection.ConnectFailure` +""" -.. py:exception:: Unauthorized +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(ClientException): + """Error in validation on API client side.""" + + pass + + +class UnsupportedVersion(ClientException): + """User is trying to use an unsupported version of the API.""" + + pass + + +class CommandError(ClientException): + """Error in CLI tool.""" + + pass + + +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 + + +EndpointException = _exc.CatalogException +"""Something is rotten in Service Catalog. + +An alias of :py:exc:`keystoneauth1.exceptions.catalog.CatalogException` +""" + +EndpointNotFound = _exc.EndpointNotFound +"""Could not find requested endpoint in Service Catalog. + +An alias of :py:exc:`keystoneauth1.exceptions.catalog.EndpointNotFound` +""" + + +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 + + +HttpError = _exc.HttpError +"""The base exception class for all HTTP exceptions. + +An alias of :py:exc:`keystoneauth1.exceptions.http.HttpError` +""" + +HTTPClientError = _exc.HTTPClientError +"""Client-side HTTP error. + +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. + +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` +""" + + +class HTTPRedirection(HttpError): + """HTTP Redirection.""" + + message = _("HTTP Redirection") + + +class MultipleChoices(HTTPRedirection): + """HTTP 300 - Multiple Choices. + + Indicates multiple options for the resource that the client may follow. + """ + + http_status = 300 + message = _("Multiple Choices") + + +BadRequest = _exc.BadRequest +"""HTTP 400 - Bad Request. + +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. + +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. + +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` """ -from keystoneclient.i18n import _ -from keystoneclient.openstack.common.apiclient.exceptions import * # noqa # 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. -ConnectionError = ConnectionRefused HTTPNotImplemented = HttpNotImplemented Timeout = RequestTimeout HTTPError = HttpError @@ -41,6 +361,7 @@ class CertificateConfigError(Exception): """Error reading the certificate.""" + def __init__(self, output): self.output = output msg = _('Unable to load certificate.') @@ -49,52 +370,65 @@ def __init__(self, output): 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) -class EmptyCatalog(EndpointNotFound): - """The service catalog is empty.""" - pass - +EmptyCatalog = _exc.EmptyCatalog +"""The service catalog is empty. -class SSLError(ConnectionRefused): - """An SSL error occurred.""" +An alias of :py:exc:`keystoneauth1.exceptions.catalog.EmptyCatalog` +""" +DiscoveryFailure = _exc.DiscoveryFailure +"""Discovery of client versions failed. -class DiscoveryFailure(ClientException): - """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. -class VersionNotAvailable(DiscoveryFailure): - """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.""" -class MissingAuthPlugin(ClientException): - """An authenticated request is required but no plugin available.""" +MissingAuthPlugin = _exc.MissingAuthPlugin +"""An authenticated request is required but no plugin available. +An alias of :py:exc:`keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin` +""" -class NoMatchingPlugin(ClientException): - """There were no auth plugins that could be created from the parameters - provided. +NoMatchingPlugin = _exc.NoMatchingPlugin +"""There were no auth plugins that could be created from the parameters +provided. - :param str name: The name of the plugin that was attempted to load. +An alias of :py:exc:`keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin` +""" - .. py:attribute:: name - The name of the plugin that was attempted to load. +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. """ - def __init__(self, name): - self.name = name - msg = _('The plugin %s could not be found') % name - super(NoMatchingPlugin, self).__init__(msg) + 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): diff --git a/keystoneclient/fixture/__init__.py b/keystoneclient/fixture/__init__.py index 8ddb43acc..92034a0da 100644 --- a/keystoneclient/fixture/__init__.py +++ b/keystoneclient/fixture/__init__.py @@ -11,32 +11,48 @@ # under the License. """ -The generators in this directory produce keystone compliant structures for use -in testing. +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', +__all__ = ('DiscoveryList', 'FixtureValidationError', 'V2Discovery', 'V3Discovery', 'V2Token', 'V3Token', 'V3FederationToken', - ] + ) diff --git a/keystoneclient/fixture/discovery.py b/keystoneclient/fixture/discovery.py index eae0bcdc3..e664d2833 100644 --- a/keystoneclient/fixture/discovery.py +++ b/keystoneclient/fixture/discovery.py @@ -10,251 +10,29 @@ # License for the specific language governing permissions and limitations # under the License. -import datetime +from keystoneauth1.fixture import discovery -from oslo_utils import timeutils -from keystoneclient import utils - -__all__ = ['DiscoveryList', +__all__ = ('DiscoveryList', 'V2Discovery', 'V3Discovery', - ] - -_DEFAULT_DAYS_AGO = 30 - - -class DiscoveryBase(dict): - """The basic version discovery structure. - - All version discovery elements should have access to these values. - - :param string id: The version id for this version entry. - :param string status: The status of this entry. - :param DateTime updated: When the API was last updated. - """ - - @utils.positional() - def __init__(self, id, status=None, updated=None): - super(DiscoveryBase, self).__init__() - - self.id = id - self.status = status or 'stable' - self.updated = updated or (timeutils.utcnow() - - datetime.timedelta(days=_DEFAULT_DAYS_AGO)) - - @property - def id(self): - return self.get('id') - - @id.setter - def id(self, value): - self['id'] = value - - @property - def status(self): - return self.get('status') - - @status.setter - def status(self, value): - self['status'] = value - - @property - def links(self): - return self.setdefault('links', []) - - @property - def updated_str(self): - return self.get('updated') - - @updated_str.setter - def updated_str(self, value): - self['updated'] = value - - @property - def updated(self): - return timeutils.parse_isotime(self.updated_str) - - @updated.setter - def updated(self, value): - self.updated_str = timeutils.isotime(value) - - @utils.positional() - def add_link(self, href, rel='self', type=None): - link = {'href': href, 'rel': rel} - if type: - link['type'] = type - self.links.append(link) - return link - - @property - def media_types(self): - return self.setdefault('media-types', []) - - @utils.positional(1) - def add_media_type(self, base, type): - mt = {'base': base, 'type': type} - self.media_types.append(mt) - return mt - - -class V2Discovery(DiscoveryBase): - """A Version element for a V2 identity service endpoint. - - Provides some default values and helper methods for creating a v2.0 - endpoint version structure. Clients should use this instead of creating - their own structures. - - :param string href: The url that this entry should point to. - :param string id: The version id that should be reported. (optional) - Defaults to 'v2.0'. - :param bool html: Add HTML describedby links to the structure. - :param bool pdf: Add PDF describedby links to the structure. - - """ - - _DESC_URL = 'http://docs.openstack.org/api/openstack-identity-service/2.0/' - - @utils.positional() - def __init__(self, href, id=None, html=True, pdf=True, **kwargs): - super(V2Discovery, self).__init__(id or 'v2.0', **kwargs) - - self.add_link(href) - - if html: - self.add_html_description() - if pdf: - self.add_pdf_description() - - def add_html_description(self): - """Add the HTML described by links. - - The standard structure includes a link to a HTML document with the - API specification. Add it to this entry. - """ - self.add_link(href=self._DESC_URL + 'content', - rel='describedby', - type='text/html') - - def add_pdf_description(self): - """Add the PDF described by links. - - The standard structure includes a link to a PDF document with the - API specification. Add it to this entry. - """ - self.add_link(href=self._DESC_URL + 'identity-dev-guide-2.0.pdf', - rel='describedby', - type='application/pdf') - - -class V3Discovery(DiscoveryBase): - """A Version element for a V3 identity service endpoint. - - Provides some default values and helper methods for creating a v3 - endpoint version structure. Clients should use this instead of creating - their own structures. - - :param href: The url that this entry should point to. - :param string id: The version id that should be reported. (optional) - Defaults to 'v3.0'. - :param bool json: Add JSON media-type elements to the structure. - :param bool xml: Add XML media-type elements to the structure. - """ - - @utils.positional() - def __init__(self, href, id=None, json=True, xml=True, **kwargs): - super(V3Discovery, self).__init__(id or 'v3.0', **kwargs) - - self.add_link(href) - - if json: - self.add_json_media_type() - if xml: - self.add_xml_media_type() - - def add_json_media_type(self): - """Add the JSON media-type links. - - The standard structure includes a list of media-types that the endpoint - supports. Add JSON to the list. - """ - self.add_media_type(base='application/json', - type='application/vnd.openstack.identity-v3+json') - - def add_xml_media_type(self): - """Add the XML media-type links. - - The standard structure includes a list of media-types that the endpoint - supports. Add XML to the list. - """ - self.add_media_type(base='application/xml', - type='application/vnd.openstack.identity-v3+xml') - - -class DiscoveryList(dict): - """A List of version elements. - - Creates a correctly structured list of identity service endpoints for - use in testing with discovery. - - :param string href: The url that this should be based at. - :param bool v2: Add a v2 element. - :param bool v3: Add a v3 element. - :param string v2_status: The status to use for the v2 element. - :param DateTime v2_updated: The update time to use for the v2 element. - :param bool v2_html: True to add a html link to the v2 element. - :param bool v2_pdf: True to add a pdf link to the v2 element. - :param string v3_status: The status to use for the v3 element. - :param DateTime v3_updated: The update time to use for the v3 element. - :param bool v3_json: True to add a html link to the v2 element. - :param bool v3_xml: True to add a pdf link to the v2 element. - """ - - TEST_URL = 'http://keystone.host:5000/' - - @utils.positional(2) - def __init__(self, href=None, v2=True, v3=True, v2_id=None, v3_id=None, - v2_status=None, v2_updated=None, v2_html=True, v2_pdf=True, - v3_status=None, v3_updated=None, v3_json=True, v3_xml=True): - super(DiscoveryList, self).__init__(versions={'values': []}) - - href = href or self.TEST_URL - - if v2: - v2_href = href.rstrip('/') + '/v2.0' - self.add_v2(v2_href, id=v2_id, status=v2_status, - updated=v2_updated, html=v2_html, pdf=v2_pdf) - - if v3: - v3_href = href.rstrip('/') + '/v3' - self.add_v3(v3_href, id=v3_id, status=v3_status, - updated=v3_updated, json=v3_json, xml=v3_xml) + ) - @property - def versions(self): - return self['versions']['values'] - def add_version(self, version): - """Add a new version structure to the list. +V2Discovery = discovery.V2Discovery +"""A Version element for a V2 identity service endpoint. - :param dict version: A new version structure to add to the list. - """ - self.versions.append(version) +An alias of :py:exc:`keystoneauth1.fixture.discovery.V2Discovery` +""" - def add_v2(self, href, **kwargs): - """Add a v2 version to the list. +V3Discovery = discovery.V3Discovery +"""A Version element for a V3 identity service endpoint. - The parameters are the same as V2Discovery. - """ - obj = V2Discovery(href, **kwargs) - self.add_version(obj) - return obj +An alias of :py:exc:`keystoneauth1.fixture.discovery.V3Discovery` +""" - def add_v3(self, href, **kwargs): - """Add a v3 version to the list. +DiscoveryList = discovery.DiscoveryList +"""A List of version elements. - The parameters are the same as V3Discovery. - """ - obj = V3Discovery(href, **kwargs) - self.add_version(obj) - return obj +An alias of :py:exc:`keystoneauth1.fixture.discovery.DiscoveryList` +""" diff --git a/keystoneclient/fixture/exception.py b/keystoneclient/fixture/exception.py index 416a3cf45..99e487633 100644 --- a/keystoneclient/fixture/exception.py +++ b/keystoneclient/fixture/exception.py @@ -10,11 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from keystoneauth1.fixture import exception -class FixtureValidationError(Exception): - """The token you created is not legitimate. - The data contained in the token that was generated is not valid and would - not have been returned from a keystone server. You should not do testing - with this token. - """ +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 index 3022f2c89..9fbf4e005 100644 --- a/keystoneclient/fixture/v2.py +++ b/keystoneclient/fixture/v2.py @@ -10,236 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -import datetime -import uuid +from keystoneauth1.fixture import v2 -from oslo_utils import timeutils -from keystoneclient.fixture import exception +Token = v2.Token +"""A V2 Keystone token that can be used for testing. - -class _Service(dict): - - def add_endpoint(self, public, admin=None, internal=None, - tenant_id=None, region=None, id=None): - data = {'tenantId': tenant_id or uuid.uuid4().hex, - 'publicURL': public, - 'adminURL': admin or public, - 'internalURL': internal or public, - 'region': region, - 'id': id or uuid.uuid4().hex} - - self.setdefault('endpoints', []).append(data) - return data - - -class Token(dict): - """A V2 Keystone token that can be used for testing. - - This object is designed to allow clients to generate a correct V2 token for - use in there test code. It should prevent clients from having to know the - correct token format and allow them to test the portions of token handling - that matter to them and not copy and paste sample. - """ - - def __init__(self, token_id=None, expires=None, issued=None, - tenant_id=None, tenant_name=None, user_id=None, - user_name=None, trust_id=None, trustee_user_id=None, - audit_id=None, audit_chain_id=None): - super(Token, self).__init__() - - self.token_id = token_id or uuid.uuid4().hex - self.user_id = user_id or uuid.uuid4().hex - self.user_name = user_name or uuid.uuid4().hex - self.audit_id = audit_id or uuid.uuid4().hex - - if not issued: - issued = timeutils.utcnow() - datetime.timedelta(minutes=2) - if not expires: - expires = issued + datetime.timedelta(hours=1) - - try: - self.issued = issued - except (TypeError, AttributeError): - # issued should be able to be passed as a string so ignore - self.issued_str = issued - - try: - self.expires = expires - except (TypeError, AttributeError): - # expires should be able to be passed as a string so ignore - self.expires_str = expires - - if tenant_id or tenant_name: - self.set_scope(tenant_id, tenant_name) - - if trust_id or trustee_user_id: - # the trustee_user_id will generally be the same as the user_id as - # the token is being issued to the trustee - self.set_trust(id=trust_id, - trustee_user_id=trustee_user_id or user_id) - - if audit_chain_id: - self.audit_chain_id = audit_chain_id - - @property - def root(self): - return self.setdefault('access', {}) - - @property - def _token(self): - return self.root.setdefault('token', {}) - - @property - def token_id(self): - return self._token['id'] - - @token_id.setter - def token_id(self, value): - self._token['id'] = value - - @property - def expires_str(self): - return self._token['expires'] - - @expires_str.setter - def expires_str(self, value): - self._token['expires'] = value - - @property - def expires(self): - return timeutils.parse_isotime(self.expires_str) - - @expires.setter - def expires(self, value): - self.expires_str = timeutils.isotime(value) - - @property - def issued_str(self): - return self._token['issued_at'] - - @issued_str.setter - def issued_str(self, value): - self._token['issued_at'] = value - - @property - def issued(self): - return timeutils.parse_isotime(self.issued_str) - - @issued.setter - def issued(self, value): - self.issued_str = timeutils.isotime(value) - - @property - def _user(self): - return self.root.setdefault('user', {}) - - @property - def user_id(self): - return self._user['id'] - - @user_id.setter - def user_id(self, value): - self._user['id'] = value - - @property - def user_name(self): - return self._user['name'] - - @user_name.setter - def user_name(self, value): - self._user['name'] = value - - @property - def tenant_id(self): - return self._token.get('tenant', {}).get('id') - - @tenant_id.setter - def tenant_id(self, value): - self._token.setdefault('tenant', {})['id'] = value - - @property - def tenant_name(self): - return self._token.get('tenant', {}).get('name') - - @tenant_name.setter - def tenant_name(self, value): - self._token.setdefault('tenant', {})['name'] = value - - @property - def _metadata(self): - return self.root.setdefault('metadata', {}) - - @property - def trust_id(self): - return self.root.setdefault('trust', {}).get('id') - - @trust_id.setter - def trust_id(self, value): - self.root.setdefault('trust', {})['id'] = value - - @property - def trustee_user_id(self): - return self.root.setdefault('trust', {}).get('trustee_user_id') - - @trustee_user_id.setter - def trustee_user_id(self, value): - self.root.setdefault('trust', {})['trustee_user_id'] = value - - @property - def audit_id(self): - try: - return self._token.get('audit_ids', [])[0] - except IndexError: - return None - - @audit_id.setter - def audit_id(self, value): - audit_chain_id = self.audit_chain_id - lval = [value] if audit_chain_id else [value, audit_chain_id] - self._token['audit_ids'] = lval - - @property - def audit_chain_id(self): - try: - return self._token.get('audit_ids', [])[1] - except IndexError: - return None - - @audit_chain_id.setter - def audit_chain_id(self, value): - self._token['audit_ids'] = [self.audit_id, value] - - def validate(self): - scoped = 'tenant' in self.token - catalog = self.root.get('serviceCatalog') - - if catalog and not scoped: - msg = 'You cannot have a service catalog on an unscoped token' - raise exception.FixtureValidationError(msg) - - if scoped and not self.user.get('roles'): - msg = 'You must have roles on a token to scope it' - raise exception.FixtureValidationError(msg) - - def add_role(self, name=None, id=None): - id = id or uuid.uuid4().hex - name = name or uuid.uuid4().hex - roles = self._user.setdefault('roles', []) - roles.append({'name': name}) - self._metadata.setdefault('roles', []).append(id) - return {'id': id, 'name': name} - - def add_service(self, type, name=None): - name = name or uuid.uuid4().hex - service = _Service(name=name, type=type) - self.root.setdefault('serviceCatalog', []).append(service) - return service - - def set_scope(self, id=None, name=None): - self.tenant_id = id or uuid.uuid4().hex - self.tenant_name = name or uuid.uuid4().hex - - def set_trust(self, id=None, trustee_user_id=None): - self.trust_id = id or uuid.uuid4().hex - self.trustee_user_id = trustee_user_id or uuid.uuid4().hex +An alias of :py:exc:`keystoneauth1.fixture.v2.Token` +""" diff --git a/keystoneclient/fixture/v3.py b/keystoneclient/fixture/v3.py index a1781dd6e..596f3e2b5 100644 --- a/keystoneclient/fixture/v3.py +++ b/keystoneclient/fixture/v3.py @@ -10,404 +10,17 @@ # License for the specific language governing permissions and limitations # under the License. -import datetime -import uuid +from keystoneauth1.fixture import v3 -from oslo_utils import timeutils -from keystoneclient.fixture import exception +Token = v3.Token +"""A V3 Keystone token that can be used for testing. +An alias of :py:exc:`keystoneauth1.fixture.v3.Token` +""" -class _Service(dict): - """One of the services that exist in the catalog. +V3FederationToken = v3.V3FederationToken +"""A V3 Keystone Federation token that can be used for testing. - You use this by adding a service to a token which returns an instance of - this object and then you can add_endpoints to the service. - """ - - def add_endpoint(self, interface, url, region=None, id=None): - data = {'id': id or uuid.uuid4().hex, - 'interface': interface, - 'url': url, - 'region': region, - 'region_id': region} - self.setdefault('endpoints', []).append(data) - return data - - def add_standard_endpoints(self, public=None, admin=None, internal=None, - region=None): - ret = [] - - if public: - ret.append(self.add_endpoint('public', public, region=region)) - if admin: - ret.append(self.add_endpoint('admin', admin, region=region)) - if internal: - ret.append(self.add_endpoint('internal', internal, region=region)) - - return ret - - -class Token(dict): - """A V3 Keystone token that can be used for testing. - - This object is designed to allow clients to generate a correct V3 token for - use in there test code. It should prevent clients from having to know the - correct token format and allow them to test the portions of token handling - that matter to them and not copy and paste sample. - """ - - def __init__(self, expires=None, issued=None, user_id=None, user_name=None, - user_domain_id=None, user_domain_name=None, methods=None, - project_id=None, project_name=None, project_domain_id=None, - project_domain_name=None, domain_id=None, domain_name=None, - trust_id=None, trust_impersonation=None, trustee_user_id=None, - trustor_user_id=None, oauth_access_token_id=None, - oauth_consumer_id=None, audit_id=None, audit_chain_id=None): - super(Token, self).__init__() - - self.user_id = user_id or uuid.uuid4().hex - self.user_name = user_name or uuid.uuid4().hex - self.user_domain_id = user_domain_id or uuid.uuid4().hex - self.user_domain_name = user_domain_name or uuid.uuid4().hex - self.audit_id = audit_id or uuid.uuid4().hex - - if not methods: - methods = ['password'] - self.methods.extend(methods) - - if not issued: - issued = timeutils.utcnow() - datetime.timedelta(minutes=2) - - try: - self.issued = issued - except (TypeError, AttributeError): - # issued should be able to be passed as a string so ignore - self.issued_str = issued - - if not expires: - expires = self.issued + datetime.timedelta(hours=1) - - try: - self.expires = expires - except (TypeError, AttributeError): - # expires should be able to be passed as a string so ignore - self.expires_str = expires - - if (project_id or project_name or - project_domain_id or project_domain_name): - self.set_project_scope(id=project_id, - name=project_name, - domain_id=project_domain_id, - domain_name=project_domain_name) - - if domain_id or domain_name: - self.set_domain_scope(id=domain_id, name=domain_name) - - if (trust_id or (trust_impersonation is not None) or - trustee_user_id or trustor_user_id): - self.set_trust_scope(id=trust_id, - impersonation=trust_impersonation, - trustee_user_id=trustee_user_id, - trustor_user_id=trustor_user_id) - - if oauth_access_token_id or oauth_consumer_id: - self.set_oauth(access_token_id=oauth_access_token_id, - consumer_id=oauth_consumer_id) - - if audit_chain_id: - self.audit_chain_id = audit_chain_id - - @property - def root(self): - return self.setdefault('token', {}) - - @property - def expires_str(self): - return self.root.get('expires_at') - - @expires_str.setter - def expires_str(self, value): - self.root['expires_at'] = value - - @property - def expires(self): - return timeutils.parse_isotime(self.expires_str) - - @expires.setter - def expires(self, value): - self.expires_str = timeutils.isotime(value, subsecond=True) - - @property - def issued_str(self): - return self.root.get('issued_at') - - @issued_str.setter - def issued_str(self, value): - self.root['issued_at'] = value - - @property - def issued(self): - return timeutils.parse_isotime(self.issued_str) - - @issued.setter - def issued(self, value): - self.issued_str = timeutils.isotime(value, subsecond=True) - - @property - def _user(self): - return self.root.setdefault('user', {}) - - @property - def user_id(self): - return self._user.get('id') - - @user_id.setter - def user_id(self, value): - self._user['id'] = value - - @property - def user_name(self): - return self._user.get('name') - - @user_name.setter - def user_name(self, value): - self._user['name'] = value - - @property - def _user_domain(self): - return self._user.setdefault('domain', {}) - - @property - def user_domain_id(self): - return self._user_domain.get('id') - - @user_domain_id.setter - def user_domain_id(self, value): - self._user_domain['id'] = value - - @property - def user_domain_name(self): - return self._user_domain.get('name') - - @user_domain_name.setter - def user_domain_name(self, value): - self._user_domain['name'] = value - - @property - def methods(self): - return self.root.setdefault('methods', []) - - @property - def project_id(self): - return self.root.get('project', {}).get('id') - - @project_id.setter - def project_id(self, value): - self.root.setdefault('project', {})['id'] = value - - @property - def project_name(self): - return self.root.get('project', {}).get('name') - - @project_name.setter - def project_name(self, value): - self.root.setdefault('project', {})['name'] = value - - @property - def project_domain_id(self): - return self.root.get('project', {}).get('domain', {}).get('id') - - @project_domain_id.setter - def project_domain_id(self, value): - project = self.root.setdefault('project', {}) - project.setdefault('domain', {})['id'] = value - - @property - def project_domain_name(self): - return self.root.get('project', {}).get('domain', {}).get('name') - - @project_domain_name.setter - def project_domain_name(self, value): - project = self.root.setdefault('project', {}) - project.setdefault('domain', {})['name'] = value - - @property - def domain_id(self): - return self.root.get('domain', {}).get('id') - - @domain_id.setter - def domain_id(self, value): - self.root.setdefault('domain', {})['id'] = value - - @property - def domain_name(self): - return self.root.get('domain', {}).get('name') - - @domain_name.setter - def domain_name(self, value): - self.root.setdefault('domain', {})['name'] = value - - @property - def trust_id(self): - return self.root.get('OS-TRUST:trust', {}).get('id') - - @trust_id.setter - def trust_id(self, value): - self.root.setdefault('OS-TRUST:trust', {})['id'] = value - - @property - def trust_impersonation(self): - return self.root.get('OS-TRUST:trust', {}).get('impersonation') - - @trust_impersonation.setter - def trust_impersonation(self, value): - self.root.setdefault('OS-TRUST:trust', {})['impersonation'] = value - - @property - def trustee_user_id(self): - trust = self.root.get('OS-TRUST:trust', {}) - return trust.get('trustee_user', {}).get('id') - - @trustee_user_id.setter - def trustee_user_id(self, value): - trust = self.root.setdefault('OS-TRUST:trust', {}) - trust.setdefault('trustee_user', {})['id'] = value - - @property - def trustor_user_id(self): - trust = self.root.get('OS-TRUST:trust', {}) - return trust.get('trustor_user', {}).get('id') - - @trustor_user_id.setter - def trustor_user_id(self, value): - trust = self.root.setdefault('OS-TRUST:trust', {}) - trust.setdefault('trustor_user', {})['id'] = value - - @property - def oauth_access_token_id(self): - return self.root.get('OS-OAUTH1', {}).get('access_token_id') - - @oauth_access_token_id.setter - def oauth_access_token_id(self, value): - self.root.setdefault('OS-OAUTH1', {})['access_token_id'] = value - - @property - def oauth_consumer_id(self): - return self.root.get('OS-OAUTH1', {}).get('consumer_id') - - @oauth_consumer_id.setter - def oauth_consumer_id(self, value): - self.root.setdefault('OS-OAUTH1', {})['consumer_id'] = value - - @property - def audit_id(self): - try: - return self.root.get('audit_ids', [])[0] - except IndexError: - return None - - @audit_id.setter - def audit_id(self, value): - audit_chain_id = self.audit_chain_id - lval = [value] if audit_chain_id else [value, audit_chain_id] - self.root['audit_ids'] = lval - - @property - def audit_chain_id(self): - try: - return self.root.get('audit_ids', [])[1] - except IndexError: - return None - - @audit_chain_id.setter - def audit_chain_id(self, value): - self.root['audit_ids'] = [self.audit_id, value] - - def validate(self): - project = self.root.get('project') - domain = self.root.get('domain') - trust = self.root.get('OS-TRUST:trust') - catalog = self.root.get('catalog') - roles = self.root.get('roles') - scoped = project or domain or trust - - if sum((bool(project), bool(domain), bool(trust))) > 1: - msg = 'You cannot scope to multiple targets' - raise exception.FixtureValidationError(msg) - - if catalog and not scoped: - msg = 'You cannot have a service catalog on an unscoped token' - raise exception.FixtureValidationError(msg) - - if scoped and not self.user.get('roles'): - msg = 'You must have roles on a token to scope it' - raise exception.FixtureValidationError(msg) - - if bool(scoped) != bool(roles): - msg = 'You must be scoped to have roles and vice-versa' - raise exception.FixtureValidationError(msg) - - def add_role(self, name=None, id=None): - roles = self.root.setdefault('roles', []) - data = {'id': id or uuid.uuid4().hex, - 'name': name or uuid.uuid4().hex} - roles.append(data) - return data - - def add_service(self, type, name=None, id=None): - service = _Service(type=type, id=id or uuid.uuid4().hex) - if name: - service['name'] = name - self.root.setdefault('catalog', []).append(service) - return service - - def set_project_scope(self, id=None, name=None, domain_id=None, - domain_name=None): - self.project_id = id or uuid.uuid4().hex - self.project_name = name or uuid.uuid4().hex - self.project_domain_id = domain_id or uuid.uuid4().hex - self.project_domain_name = domain_name or uuid.uuid4().hex - - def set_domain_scope(self, id=None, name=None): - self.domain_id = id or uuid.uuid4().hex - self.domain_name = name or uuid.uuid4().hex - - def set_trust_scope(self, id=None, impersonation=False, - trustee_user_id=None, trustor_user_id=None): - self.trust_id = id or uuid.uuid4().hex - self.trust_impersonation = impersonation - self.trustee_user_id = trustee_user_id or uuid.uuid4().hex - self.trustor_user_id = trustor_user_id or uuid.uuid4().hex - - def set_oauth(self, access_token_id=None, consumer_id=None): - self.oauth_access_token_id = access_token_id or uuid.uuid4().hex - self.oauth_consumer_id = consumer_id or uuid.uuid4().hex - - -class V3FederationToken(Token): - """A V3 Keystone Federation token that can be used for testing. - - Similar to V3Token, this object is designed to allow clients to generate - a correct V3 federation token for use in test code. - """ - - def __init__(self, methods=None, identity_provider=None, protocol=None, - groups=None): - methods = methods or ['saml2'] - super(V3FederationToken, self).__init__(methods=methods) - # NOTE(stevemar): Federated tokens do not have a domain for the user - del self._user['domain'] - self.add_federation_info_to_user(identity_provider, protocol, groups) - - def add_federation_info_to_user(self, identity_provider=None, - protocol=None, groups=None): - data = { - "OS-FEDERATION": { - "identity_provider": identity_provider or uuid.uuid4().hex, - "protocol": protocol or uuid.uuid4().hex, - "groups": groups or [{"id": uuid.uuid4().hex}] - } - } - self._user.update(data) - return data +An alias of :py:exc:`keystoneauth1.fixture.v3.V3FederationToken` +""" diff --git a/keystoneclient/generic/__init__.py b/keystoneclient/generic/__init__.py index ed59d727c..745692105 100644 --- a/keystoneclient/generic/__init__.py +++ b/keystoneclient/generic/__init__.py @@ -1,4 +1,4 @@ -__all__ = [ +__all__ = ( 'client', -] +) diff --git a/keystoneclient/generic/client.py b/keystoneclient/generic/client.py index 7ca39fbce..7c82c195f 100644 --- a/keystoneclient/generic/client.py +++ b/keystoneclient/generic/client.py @@ -14,17 +14,23 @@ # under the License. import logging +import urllib.parse as urlparse -from six.moves.urllib import parse as urlparse +from debtcollector import removals from keystoneclient import exceptions from keystoneclient import httpclient -from keystoneclient.i18n import _, _LE +from keystoneclient.i18n import _ _logger = logging.getLogger(__name__) +# 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. @@ -75,18 +81,18 @@ 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.""" + """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: - resp, body = self.request(url, "GET", - headers={'Accept': - 'application/json'}) + 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 @@ -125,7 +131,7 @@ def _check_keystone_versions(self, url): else: raise exceptions.from_response(resp, "GET", url) except Exception: - _logger.exception(_LE('Failed to detect available versions.')) + _logger.exception('Failed to detect available versions.') def discover_extensions(self, url=None): """Discover Keystone extensions supported. @@ -144,13 +150,13 @@ 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: if not url.endswith("/"): url += '/' - resp, body = self.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 if 'extensions' in body and 'values' in body['extensions']: # Parse correct format (per contract) @@ -169,11 +175,11 @@ def _check_keystone_extensions(self, url): raise exceptions.from_response( resp, "GET", "%sextensions" % url) except Exception: - _logger.exception(_LE('Failed to check keystone extensions.')) + _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 @@ -192,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 e5330a566..000000000 --- a/keystoneclient/generic/shell.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2010 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. - -import six - -from keystoneclient.generic import client -from keystoneclient.i18n import _ -from keystoneclient import utils - - -CLIENT_CLASS = client.Client - - -@utils.unauthenticated -def do_discover(cs, args): - """Discover Keystone servers, supported API versions and 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 six.iteritems(versions): - if key != 'message': - print(_(" - supports version %(id)s (%(status)s) here " - "%(url)s") % - version) - extensions = cs.discover_extensions(version['url']) - if extensions: - for key, extension in six.iteritems(extensions): - if key != 'message': - print(_(" - and %(key)s: %(extension)s") % - {'key': key, 'extension': extension}) - else: - print(_("No Keystone-compatible endpoint found")) diff --git a/keystoneclient/hacking/checks.py b/keystoneclient/hacking/checks.py deleted file mode 100644 index 42826982a..000000000 --- a/keystoneclient/hacking/checks.py +++ /dev/null @@ -1,37 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""python-keystoneclient's pep8 extensions. - -In order to make the review process faster and easier for core devs we are -adding some python-keystoneclient specific pep8 checks. This will catch common -errors so that core devs don't have to. - -""" - - -import re - - -def check_oslo_namespace_imports(logical_line, blank_before, filename): - oslo_namespace_imports = re.compile( - r"(((from)|(import))\s+oslo\.)|(from\s+oslo\s+import\s+)") - - if re.match(oslo_namespace_imports, logical_line): - msg = ("K333: '%s' must be used instead of '%s'.") % ( - logical_line.replace('oslo.', 'oslo_'), - logical_line) - yield(0, msg) - - -def factory(register): - register(check_oslo_namespace_imports) diff --git a/keystoneclient/httpclient.py b/keystoneclient/httpclient.py index 317a9e88e..8c38d1596 100644 --- a/keystoneclient/httpclient.py +++ b/keystoneclient/httpclient.py @@ -15,59 +15,71 @@ # 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. -""" +"""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 pkg_resources +import packaging.version import requests -from six.moves.urllib import parse as urlparse try: - import pickle + 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 = pkg_resources.parse_version( - pkg_resources.get_distribution("keyring").version) - keyring_min = pkg_resources.parse_version('2.1') - keyring_bad = (pkg_resources.parse_version('3.3'),) + 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, pkg_resources.DistributionNotFound): +except (ImportError, importlib.metadata.PackageNotFoundError): keyring = None pickle = None -# Python 2.5 compat fix -if not hasattr(urlparse, 'parse_qsl'): - import cgi - urlparse.parse_qsl = cgi.parse_qsl - from keystoneclient import _discover from keystoneclient import access -from keystoneclient import adapter from keystoneclient.auth import base from keystoneclient import baseclient from keystoneclient import exceptions -from keystoneclient.i18n import _, _LW +from keystoneclient.i18n import _ from keystoneclient import session as client_session -from keystoneclient import utils _logger = logging.getLogger(__name__) -# These variables are moved and using them via httpclient is deprecated. -# Maintain here for compatibility. USER_AGENT = client_session.USER_AGENT -request = client_session.request +"""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): @@ -116,23 +128,31 @@ def user_id(self): # the identity plugin case try: return self.session.auth.get_access(self.session).user_id - except AttributeError: + except AttributeError: # nosec(cjschaef): attempt legacy retrival, or + # return None pass - # there is a case that we explicity allow (tested by our unit tests) + # 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: + except AttributeError: # nosec(cjschaef): retrivals failed, return + # None pass return None class HTTPClient(baseclient.Client, base.BaseAuthPlugin): - """HTTP client + """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) @@ -152,18 +172,29 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin): :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: DEPRECATED: use session. (optional) + :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: DEPRECATED: use session. (optional) - :param string key: DEPRECATED: use session. (optional) - :param string cert: DEPRECATED: use session. (optional) - :param boolean insecure: DEPRECATED: use session. (optional) - :param string original_ip: DEPRECATED: use session. (optional) - :param boolean debug: DEPRECATED: use logging configuration. (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 @@ -178,10 +209,13 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin): keyring is about to expire. default: 30 (optional) :param string tenant_name: Tenant name. (optional) The tenant_name keyword - argument is deprecated, use project_name - instead. + 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, use project_id instead. + 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. @@ -189,7 +223,7 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin): :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 (optional) + 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) @@ -204,16 +238,19 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin): version = None - @utils.positional(enforcement=utils.positional.WARN) + @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, debug=False, auth_ref=None, use_keyring=False, + 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='admin', endpoint_override=None, auth=None, + interface='default', endpoint_override=None, auth=None, user_agent=USER_AGENT, connect_retries=None, **kwargs): # set baseline defaults self.user_id = None @@ -248,12 +285,23 @@ def __init__(self, username=None, tenant_id=None, tenant_name=None, 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 - self.auth_url = self.auth_ref.auth_url[0] - self._management_url = self.auth_ref.management_url[0] + 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 + region_name = self.auth_ref.service_catalog._region_name + else: self.auth_ref = None @@ -313,20 +361,34 @@ def __init__(self, username=None, tenant_id=None, tenant_name=None, 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 = client_session.Session._construct(kwargs) session.auth = self - super(HTTPClient, self).__init__(session=session) + self.session = session self.domain = '' - self.debug_log = debug - # 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__ 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, @@ -338,9 +400,14 @@ def __init__(self, username=None, tenant_id=None, tenant_name=None, 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(_LW('Failed to load keyring modules.')) + _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 @@ -388,28 +455,48 @@ def auth_token(self): @property def service_catalog(self): - """Returns this client's service catalog.""" - return self.auth_ref.service_catalog + """Return this client's service catalog.""" + try: + return self.auth_ref.service_catalog + except AttributeError: + return None def has_service_catalog(self): - """Returns True if this client provides a service catalog.""" + """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. - This is deprecated, use project_id instead. + + .. 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. - This is deprecated, use project_name instead. + + .. 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 - @utils.positional(enforcement=utils.positional.WARN) 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, @@ -529,10 +616,10 @@ def _build_keyring_key(self, **kwargs): Used to store and retrieve auth_ref from keyring. - Returns a slash-separated string of values ordered by key name. + Return a slash-separated string of values ordered by key name. """ - return '/'.join([kwargs[k] or '?' for k in sorted(kwargs.keys())]) + return '/'.join([kwargs[k] or '?' for k in sorted(kwargs)]) def get_auth_ref_from_keyring(self, **kwargs): """Retrieve auth_ref from keyring. @@ -552,28 +639,26 @@ def get_auth_ref_from_keyring(self, **kwargs): auth_ref = keyring.get_password("keystoneclient_auth", keyring_key) if auth_ref: - auth_ref = pickle.loads(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( - _LW('Unable to retrieve token from keyring %s'), e) + _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. - - """ + """Store auth_ref into keyring.""" if self.use_keyring: try: keyring.set_password("keystoneclient_auth", keyring_key, - pickle.dumps(self.auth_ref)) + pickle.dumps(self.auth_ref)) # nosec + # (cjschaef): see bug 1534288 except Exception as e: - _logger.warning( - _LW("Failed to store token into keyring %s"), e) + _logger.warning("Failed to store token into keyring %s", e) def _process_management_url(self, region_name): try: @@ -581,8 +666,8 @@ def _process_management_url(self, region_name): service_type='identity', endpoint_type='admin', region_name=region_name) - except exceptions.EndpointNotFound: - pass + 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. @@ -621,7 +706,6 @@ def management_url(self, value): # permanently setting _endpoint would better match that behaviour. self._endpoint = value - @utils.positional(enforcement=utils.positional.WARN) def get_raw_token_from_identity_service(self, auth_url, username=None, password=None, tenant_name=None, tenant_id=None, token=None, @@ -649,6 +733,7 @@ def get_raw_token_from_identity_service(self, auth_url, username=None, 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. @@ -656,27 +741,33 @@ def request(self, *args, **kwargs): 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): - """Makes an authenticated request to keystone endpoint by - concatenating self.management_url and url and passing in method and + """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. """ - # NOTE(jamielennox): This is deprecated and is no longer a part of the - # standard client request path. It now goes via the adapter instead. if not management: endpoint_filter = kwargs.setdefault('endpoint_filter', {}) endpoint_filter.setdefault('interface', 'public') kwargs.setdefault('authenticated', None) - return self.request(url, method, **kwargs) + 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. @@ -684,12 +775,16 @@ def get(self, url, **kwargs): authentication token if one is available. .. warning:: - *DEPRECATED*: This function is no longer used. 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. + + *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. @@ -697,12 +792,16 @@ def head(self, url, **kwargs): authentication token if one is available. .. warning:: - *DEPRECATED*: This function is no longer used. 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. + + *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. @@ -710,12 +809,16 @@ def post(self, url, **kwargs): authentication token if one is available. .. warning:: - *DEPRECATED*: This function is no longer used. 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. + + *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. @@ -723,12 +826,16 @@ def put(self, url, **kwargs): authentication token if one is available. .. warning:: - *DEPRECATED*: This function is no longer used. 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. + + *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. @@ -736,12 +843,16 @@ def patch(self, url, **kwargs): an authentication token if one is available. .. warning:: - *DEPRECATED*: This function is no longer used. 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. + + *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. @@ -749,15 +860,15 @@ def delete(self, url, **kwargs): an authentication token if one is available. .. warning:: - *DEPRECATED*: This function is no longer used. 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. + + *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) - # DEPRECATIONS: The following methods are no longer directly supported - # but maintained for compatibility purposes. - deprecated_session_variables = {'original_ip': None, 'cert': None, 'timeout': None, @@ -766,37 +877,55 @@ def delete(self, url, **kwargs): deprecated_adapter_variables = {'region_name': None} def __getattr__(self, name): - # FIXME(jamielennox): provide a proper deprecated warning + """Fetch deprecated session variables.""" try: var_name = self.deprecated_session_variables[name] - except KeyError: + 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: + 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): - # FIXME(jamielennox): provide a proper deprecated warning + """Assign value to deprecated seesion variables.""" try: var_name = self.deprecated_session_variables[name] - except KeyError: + 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: + 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 index fc9a52b3d..f3726d199 100644 --- a/keystoneclient/i18n.py +++ b/keystoneclient/i18n.py @@ -14,7 +14,7 @@ """oslo.i18n integration module. -See http://docs.openstack.org/developer/oslo.i18n/usage.html . +See https://docs.openstack.org/oslo.i18n/latest/user/index.html . """ @@ -25,13 +25,3 @@ # The primary translation function using the well-known name "_" _ = _translators.primary - -# Translators for log levels. -# -# The abbreviated names are meant to reflect the usual use of a short -# name like '_'. The "L" is for "log" and the other letter comes from -# the level. -_LI = _translators.log_info -_LW = _translators.log_warning -_LE = _translators.log_error -_LC = _translators.log_critical diff --git a/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 d0fe102e8..000000000 --- a/keystoneclient/middleware/auth_token.py +++ /dev/null @@ -1,1623 +0,0 @@ -# Copyright 2010-2012 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -TOKEN-BASED AUTH MIDDLEWARE - -.. warning:: - - This module is DEPRECATED. The auth_token middleware has been moved to the - `keystonemiddleware repository - `_. - -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 - -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_DOMAIN_ID - Identity service managed unique identifier, string. Only present if - this is a domain-scoped v3 token. - -HTTP_X_DOMAIN_NAME - Unique domain name, string. Only present if this is a domain-scoped - v3 token. - -HTTP_X_PROJECT_ID - Identity service managed unique identifier, string. Only present if - this is a project-scoped v3 token, or a tenant-scoped v2 token. - -HTTP_X_PROJECT_NAME - Project name, unique within owning domain, string. Only present if - this is a project-scoped v3 token, or a tenant-scoped v2 token. - -HTTP_X_PROJECT_DOMAIN_ID - Identity service managed unique identifier of owning domain of - project, string. Only present if this is a project-scoped v3 token. If - this variable is set, this indicates that the PROJECT_NAME can only - be assumed to be unique within this domain. - -HTTP_X_PROJECT_DOMAIN_NAME - Name of owning domain of project, string. Only present if this is a - project-scoped v3 token. If this variable is set, this indicates that - the PROJECT_NAME can only be assumed to be unique within this domain. - -HTTP_X_USER_ID - Identity-service managed unique identifier, string - -HTTP_X_USER_NAME - User identifier, unique within owning domain, string - -HTTP_X_USER_DOMAIN_ID - Identity service managed unique identifier of owning domain of - user, string. If this variable is set, this indicates that the USER_NAME - can only be assumed to be unique within this domain. - -HTTP_X_USER_DOMAIN_NAME - Name of owning domain of user, string. If this variable is set, this - indicates that the USER_NAME can only be assumed to be unique within - this domain. - -HTTP_X_ROLES - Comma delimited list of case-sensitive role names - -HTTP_X_SERVICE_CATALOG - json encoded keystone service catalog (optional). - For compatibility reasons this catalog will always be in the V2 catalog - format even if it is a v3 token. - -HTTP_X_TENANT_ID - *Deprecated* in favor of HTTP_X_PROJECT_ID - Identity service managed unique identifier, string. For v3 tokens, this - will be set to the same value as HTTP_X_PROJECT_ID - -HTTP_X_TENANT_NAME - *Deprecated* in favor of HTTP_X_PROJECT_NAME - Project identifier, unique within owning domain, string. For v3 tokens, - this will be set to the same value as HTTP_X_PROJECT_NAME - -HTTP_X_TENANT - *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME - Keystone-assigned unique identifier, string. For v3 tokens, this - will be set to the same value as HTTP_X_PROJECT_ID - -HTTP_X_USER - *Deprecated* in favor of HTTP_X_USER_ID and HTTP_X_USER_NAME - User name, unique within owning domain, string - -HTTP_X_ROLE - *Deprecated* in favor of HTTP_X_ROLES - Will contain the same values as HTTP_X_ROLES. - -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 contextlib -import datetime -import logging -import os -import stat -import tempfile -import time - -import netaddr -from oslo_config import cfg -from oslo_serialization import jsonutils -from oslo_utils import timeutils -import requests -import six -from six.moves import urllib - -from keystoneclient import access -from keystoneclient.common import cms -from keystoneclient import exceptions -from keystoneclient.middleware import memcache_crypt -from keystoneclient.openstack.common import memorycache - - -# 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. - - -# NOTE(jamielennox): A number of options below are deprecated however are left -# in the list and only mentioned as deprecated in the help string. This is -# because we have to provide the same deprecation functionality for arguments -# passed in via the conf in __init__ (from paste) and there is no way to test -# that the default value was set or not in CONF. -# Also if we were to remove the options from the CONF list (as typical CONF -# deprecation works) then other projects will not be able to override the -# options via CONF. - -opts = [ - cfg.StrOpt('auth_admin_prefix', - default='', - help='Prefix to prepend at the beginning of the path. ' - 'Deprecated, use identity_uri.'), - cfg.StrOpt('auth_host', - default='127.0.0.1', - help='Host providing the admin Identity API endpoint. ' - 'Deprecated, use identity_uri.'), - cfg.IntOpt('auth_port', - default=35357, - help='Port of the admin Identity API endpoint. ' - 'Deprecated, use identity_uri.'), - cfg.StrOpt('auth_protocol', - default='https', - help='Protocol of the admin Identity API endpoint ' - '(http or https). Deprecated, use identity_uri.'), - cfg.StrOpt('auth_uri', - default=None, - # FIXME(dolph): should be default='http://127.0.0.1:5000/v2.0/', - # or (depending on client support) an unversioned, publicly - # accessible identity endpoint (see bug 1207517) - help='Complete public Identity API endpoint'), - cfg.StrOpt('identity_uri', - default=None, - help='Complete admin Identity API endpoint. This should ' - 'specify the unversioned root endpoint ' - 'e.g. https://localhost:35357/'), - cfg.StrOpt('auth_version', - default=None, - help='API version of the admin Identity API endpoint'), - cfg.BoolOpt('delay_auth_decision', - default=False, - help='Do not handle authorization requests within the' - ' middleware, but delegate the authorization decision to' - ' downstream WSGI components'), - cfg.BoolOpt('http_connect_timeout', - default=None, - help='Request timeout value for communicating with Identity' - ' API server.'), - cfg.IntOpt('http_request_max_retries', - default=3, - help='How many times are we trying to reconnect when' - ' communicating with Identity API Server.'), - cfg.StrOpt('admin_token', - secret=True, - help='This option is deprecated and may be removed in a future' - ' release. Single shared secret with the Keystone configuration' - ' used for bootstrapping a Keystone installation, or otherwise' - ' bypassing the normal authentication process. This option' - ' should not be used, use `admin_user` and `admin_password`' - ' instead.'), - cfg.StrOpt('admin_user', - help='Keystone account username'), - cfg.StrOpt('admin_password', - secret=True, - help='Keystone account password'), - cfg.StrOpt('admin_tenant_name', - default='admin', - help='Keystone service account tenant name to validate' - ' user tokens'), - cfg.StrOpt('cache', - default=None, - help='Env key for the swift cache'), - cfg.StrOpt('certfile', - help='Required if Keystone server requires client certificate'), - cfg.StrOpt('keyfile', - help='Required if Keystone server requires client certificate'), - cfg.StrOpt('cafile', default=None, - help='A PEM encoded Certificate Authority to use when ' - 'verifying HTTPs connections. Defaults to system CAs.'), - cfg.BoolOpt('insecure', default=False, help='Verify HTTPS connections.'), - cfg.StrOpt('signing_dir', - help='Directory used to cache files related to PKI tokens'), - cfg.ListOpt('memcached_servers', - deprecated_name='memcache_servers', - help='Optionally specify a list of memcached server(s) to' - ' use for caching. If left undefined, tokens will instead be' - ' cached in-process.'), - cfg.IntOpt('token_cache_time', - default=300, - help='In order to prevent excessive effort spent validating' - ' tokens, the middleware caches previously-seen tokens for a' - ' configurable duration (in seconds). Set to -1 to disable' - ' caching completely.'), - cfg.IntOpt('revocation_cache_time', - default=10, - help='Determines the frequency at which the list of revoked' - ' tokens is retrieved from the Identity service (in seconds). A' - ' high number of revocation events combined with a low cache' - ' duration may significantly reduce performance.'), - cfg.StrOpt('memcache_security_strategy', - default=None, - help='(optional) if defined, indicate whether token data' - ' should be authenticated or authenticated and encrypted.' - ' Acceptable values are MAC or ENCRYPT. If MAC, token data is' - ' authenticated (with HMAC) in the cache. If ENCRYPT, token' - ' data is encrypted and authenticated in the cache. If the' - ' value is not one of these options or empty, auth_token will' - ' raise an exception on initialization.'), - cfg.StrOpt('memcache_secret_key', - default=None, - secret=True, - help='(optional, mandatory if memcache_security_strategy is' - ' defined) this string is used for key derivation.'), - cfg.BoolOpt('include_service_catalog', - default=True, - help='(optional) indicate whether to set the X-Service-Catalog' - ' header. If False, middleware will not ask for service' - ' catalog on token validation and will not set the' - ' X-Service-Catalog header.'), - cfg.StrOpt('enforce_token_bind', - default='permissive', - help='Used to control the use and type of token binding. Can' - ' be set to: "disabled" to not check token binding.' - ' "permissive" (default) to validate binding information if the' - ' bind type is of a form known to the server and ignore it if' - ' not. "strict" like "permissive" but if the bind type is' - ' unknown the token will be rejected. "required" any form of' - ' token binding is needed to be allowed. Finally the name of a' - ' binding method that must be present in tokens.'), - cfg.BoolOpt('check_revocations_for_cached', default=False, - help='If true, the revocation list will be checked for cached' - ' tokens. This requires that PKI tokens are configured on the' - ' Keystone server.'), - cfg.ListOpt('hash_algorithms', default=['md5'], - help='Hash algorithms to use for hashing PKI tokens. This may' - ' be a single algorithm or multiple. The algorithms are those' - ' supported by Python standard hashlib.new(). The hashes will' - ' be tried in the order given, so put the preferred one first' - ' for performance. The result of the first hash will be stored' - ' in the cache. This will typically be set to multiple values' - ' only while migrating from a less secure algorithm to a more' - ' secure one. Once all the old tokens are expired this option' - ' should be set to a single value for better performance.'), -] - -CONF = cfg.CONF -CONF.register_opts(opts, group='keystone_authtoken') - -LIST_OF_VERSIONS_TO_ATTEMPT = ['v2.0', 'v3.0'] -CACHE_KEY_TEMPLATE = 'tokens/%s' - - -class BIND_MODE(object): - DISABLED = 'disabled' - PERMISSIVE = 'permissive' - STRICT = 'strict' - REQUIRED = 'required' - KERBEROS = 'kerberos' - - -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 _token_is_v2(token_info): - return ('access' in token_info) - - -def _token_is_v3(token_info): - return ('token' in token_info) - - -def confirm_token_not_expired(data): - if not data: - raise InvalidUserToken('Token authorization failed') - if _token_is_v2(data): - timestamp = data['access']['token']['expires'] - elif _token_is_v3(data): - timestamp = data['token']['expires_at'] - else: - raise InvalidUserToken('Token authorization failed') - expires = timeutils.parse_isotime(timestamp) - expires = timeutils.normalize_time(expires) - utcnow = timeutils.utcnow() - if utcnow >= expires: - raise InvalidUserToken('Token authorization failed') - return timeutils.isotime(at=expires, subsecond=True) - - -def _v3_to_v2_catalog(catalog): - """Convert a catalog to v2 format. - - X_SERVICE_CATALOG must be specified in v2 format. If you get a token - that is in v3 convert it. - """ - v2_services = [] - for v3_service in catalog: - # first copy over the entries we allow for the service - v2_service = {'type': v3_service['type']} - try: - v2_service['name'] = v3_service['name'] - except KeyError: - pass - - # now convert the endpoints. Because in v3 we specify region per - # URL not per group we have to collect all the entries of the same - # region together before adding it to the new service. - regions = {} - for v3_endpoint in v3_service.get('endpoints', []): - region_name = v3_endpoint.get('region') - try: - region = regions[region_name] - except KeyError: - region = {'region': region_name} if region_name else {} - regions[region_name] = region - - interface_name = v3_endpoint['interface'].lower() + 'URL' - region[interface_name] = v3_endpoint['url'] - - v2_service['endpoints'] = list(regions.values()) - v2_services.append(v2_service) - - return v2_services - - -def safe_quote(s): - """URL-encode strings that are not already URL-encoded.""" - return urllib.parse.quote(s) if s == urllib.parse.unquote(s) else s - - -def _conf_values_type_convert(conf): - """Convert conf values into correct type.""" - if not conf: - return {} - _opts = {} - opt_types = dict((o.dest, getattr(o, 'type', str)) for o in opts) - for k, v in six.iteritems(conf): - try: - if v is None: - _opts[k] = v - else: - _opts[k] = opt_types[k](v) - except KeyError: - _opts[k] = v - except ValueError as e: - raise ConfigurationError( - 'Unable to convert the value of %s option into correct ' - 'type: %s' % (k, e)) - return _opts - - -class InvalidUserToken(Exception): - pass - - -class ServiceError(Exception): - pass - - -class ConfigurationError(Exception): - pass - - -class NetworkError(Exception): - pass - - -class MiniResp(object): - def __init__(self, error_message, env, headers=[]): - # The HEAD method is unique: it must never return a body, even if - # it reports an error (RFC-2616 clause 9.4). We relieve callers - # from varying the error responses depending on the method. - if env['REQUEST_METHOD'] == 'HEAD': - self.body = [''] - else: - self.body = [error_message] - self.headers = list(headers) - self.headers.append(('Content-type', 'text/plain')) - - -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.LOG.warning( - 'This middleware module is deprecated as of v0.10.0 in favor of ' - 'keystonemiddleware.auth_token - please update your WSGI pipeline ' - 'to reference the new middleware package.') - # NOTE(wanghong): If options are set in paste file, all the option - # values passed into conf are string type. So, we should convert the - # conf value into correct type. - self.conf = _conf_values_type_convert(conf) - self.app = app - - # delay_auth_decision means we still allow unauthenticated requests - # through and we let the downstream service make the final decision - self.delay_auth_decision = (self._conf_get('delay_auth_decision') in - (True, 'true', 't', '1', 'on', 'yes', 'y')) - - # where to find the auth service (we use this to validate tokens) - self.identity_uri = self._conf_get('identity_uri') - self.auth_uri = self._conf_get('auth_uri') - - # NOTE(jamielennox): it does appear here that our defaults arguments - # are backwards. We need to do it this way so that we can handle the - # same deprecation strategy for CONF and the conf variable. - if not self.identity_uri: - self.LOG.warning('Configuring admin URI using auth fragments. ' - 'This is deprecated, use \'identity_uri\'' - ' instead.') - - auth_host = self._conf_get('auth_host') - auth_port = int(self._conf_get('auth_port')) - auth_protocol = self._conf_get('auth_protocol') - auth_admin_prefix = self._conf_get('auth_admin_prefix') - - if netaddr.valid_ipv6(auth_host): - # Note(dzyu) it is an IPv6 address, so it needs to be wrapped - # with '[]' to generate a valid IPv6 URL, based on - # http://www.ietf.org/rfc/rfc2732.txt - auth_host = '[%s]' % auth_host - - self.identity_uri = '%s://%s:%s' % (auth_protocol, auth_host, - auth_port) - if auth_admin_prefix: - self.identity_uri = '%s/%s' % (self.identity_uri, - auth_admin_prefix.strip('/')) - else: - self.identity_uri = self.identity_uri.rstrip('/') - - if self.auth_uri is None: - self.LOG.warning( - 'Configuring auth_uri to point to the public identity ' - 'endpoint is required; clients may not be able to ' - 'authenticate against an admin endpoint') - - # FIXME(dolph): drop support for this fallback behavior as - # documented in bug 1207517. - # NOTE(jamielennox): we urljoin '/' to get just the base URI as - # this is the original behaviour. - self.auth_uri = urllib.parse.urljoin(self.identity_uri, '/') - self.auth_uri = self.auth_uri.rstrip('/') - - # SSL - self.cert_file = self._conf_get('certfile') - self.key_file = self._conf_get('keyfile') - self.ssl_ca_file = self._conf_get('cafile') - self.ssl_insecure = self._conf_get('insecure') - - # signing - self.signing_dirname = self._conf_get('signing_dir') - if self.signing_dirname is None: - self.signing_dirname = tempfile.mkdtemp(prefix='keystone-signing-') - self.LOG.info('Using %s as cache directory for signing certificate', - self.signing_dirname) - self.verify_signing_dir() - - val = '%s/signing_cert.pem' % self.signing_dirname - self.signing_cert_file_name = val - val = '%s/cacert.pem' % self.signing_dirname - self.signing_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') - if self.admin_token: - self.LOG.warning( - "The admin_token option in the auth_token middleware is " - "deprecated and should not be used. The admin_user and " - "admin_password options should be used instead. The " - "admin_token option may be removed in a future release.") - self.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') - - memcache_security_strategy = ( - self._conf_get('memcache_security_strategy')) - - self._token_cache = TokenCache( - self.LOG, - cache_time=int(self._conf_get('token_cache_time')), - hash_algorithms=self._conf_get('hash_algorithms'), - env_cache_name=self._conf_get('cache'), - memcached_servers=self._conf_get('memcached_servers'), - memcache_security_strategy=memcache_security_strategy, - memcache_secret_key=self._conf_get('memcache_secret_key')) - - self._token_revocation_list = None - self._token_revocation_list_fetched_time = None - self.token_revocation_list_cache_timeout = datetime.timedelta( - seconds=self._conf_get('revocation_cache_time')) - http_connect_timeout_cfg = self._conf_get('http_connect_timeout') - self.http_connect_timeout = (http_connect_timeout_cfg and - int(http_connect_timeout_cfg)) - self.auth_version = None - self.http_request_max_retries = ( - self._conf_get('http_request_max_retries')) - - self.include_service_catalog = self._conf_get( - 'include_service_catalog') - - self.check_revocations_for_cached = self._conf_get( - 'check_revocations_for_cached') - - 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 _choose_api_version(self): - """Determine the api version that we should use.""" - - # If the configuration specifies an auth_version we will just - # assume that is correct and use it. We could, of course, check - # that this version is supported by the server, but in case - # there are some problems in the field, we want as little code - # as possible in the way of letting auth_token talk to the - # server. - if self._conf_get('auth_version'): - version_to_use = self._conf_get('auth_version') - self.LOG.info('Auth Token proceeding with requested %s apis', - version_to_use) - else: - version_to_use = None - versions_supported_by_server = self._get_supported_versions() - if versions_supported_by_server: - for version in LIST_OF_VERSIONS_TO_ATTEMPT: - if version in versions_supported_by_server: - version_to_use = version - break - if version_to_use: - self.LOG.info('Auth Token confirmed use of %s apis', - version_to_use) - else: - self.LOG.error( - 'Attempted versions [%s] not in list supported by ' - 'server [%s]', - ', '.join(LIST_OF_VERSIONS_TO_ATTEMPT), - ', '.join(versions_supported_by_server)) - raise ServiceError('No compatible apis supported by server') - return version_to_use - - def _get_supported_versions(self): - versions = [] - response, data = self._json_request('GET', '/') - if response.status_code == 501: - self.LOG.warning('Old keystone installation found...assuming v2.0') - versions.append('v2.0') - elif response.status_code != 300: - self.LOG.error('Unable to get version info from keystone: %s', - response.status_code) - raise ServiceError('Unable to get version info from keystone') - else: - try: - for version in data['versions']['values']: - versions.append(version['id']) - except KeyError: - self.LOG.error( - 'Invalid version response format from server') - raise ServiceError('Unable to parse version response ' - 'from keystone') - - self.LOG.debug('Server reports support for api versions: %s', - ', '.join(versions)) - return versions - - 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') - - self._token_cache.initialize(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) - 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 = MiniResp('Service unavailable', env) - start_response('503 Service Unavailable', resp.headers) - return resp.body - - 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-Domain-Id', - 'X-Domain-Name', - 'X-Project-Id', - 'X-Project-Name', - 'X-Project-Domain-Id', - 'X-Project-Domain-Name', - 'X-User-Id', - 'X-User-Name', - 'X-User-Domain-Id', - 'X-User-Domain-Name', - 'X-Roles', - 'X-Service-Catalog', - # Deprecated - 'X-User', - 'X-Tenant-Id', - 'X-Tenant-Name', - '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.warning('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 = MiniResp('Authentication required', env, headers) - start_response('401 Unauthorized', resp.headers) - return resp.body - - 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 _http_request(self, method, path, **kwargs): - """HTTP request helper used to make unspecified content type requests. - - :param method: http method - :param path: relative request url - :return (http response object, response body) - :raise ServerError when unable to communicate with keystone - - """ - url = '%s/%s' % (self.identity_uri, path.lstrip('/')) - - kwargs.setdefault('timeout', self.http_connect_timeout) - if self.cert_file and self.key_file: - kwargs['cert'] = (self.cert_file, self.key_file) - elif self.cert_file or self.key_file: - self.LOG.warning('Cannot use only a cert or key file. ' - 'Please provide both. Ignoring.') - - kwargs['verify'] = self.ssl_ca_file or True - if self.ssl_insecure: - kwargs['verify'] = False - - RETRIES = self.http_request_max_retries - retry = 0 - while True: - try: - response = requests.request(method, url, **kwargs) - break - except Exception as e: - if retry >= RETRIES: - self.LOG.error('HTTP connection exception: %s', e) - raise NetworkError('Unable to communicate with keystone') - # NOTE(vish): sleep 0.5, 1, 2 - self.LOG.warning('Retrying on HTTP connection exception: %s', - e) - time.sleep(2.0 ** retry / 2) - retry += 1 - - return response - - 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 - - """ - kwargs = { - 'headers': { - 'Content-type': 'application/json', - 'Accept': 'application/json', - }, - } - - if additional_headers: - kwargs['headers'].update(additional_headers) - - if body: - kwargs['data'] = jsonutils.dumps(body) - - response = self._http_request(method, path, **kwargs) - - try: - data = jsonutils.loads(response.text) - 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 - - Irrespective of the auth version we are going to use for the - user token, for simplicity we always use a v2 admin token to - validate the user token. - - """ - 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'] - if not (token and expiry): - raise AssertionError('invalid token or expire') - datetime_expiry = timeutils.parse_isotime(expiry) - return (token, timeutils.normalize_time(datetime_expiry)) - except (AssertionError, KeyError): - self.LOG.warning( - 'Unexpected response from keystone service: %s', data) - raise ServiceError('invalid json response') - except (ValueError): - data['access']['token']['id'] = '' - self.LOG.warning( - 'Unable to parse expiration time from token: %s', data) - raise ServiceError('invalid json response') - - def _validate_user_token(self, user_token, env, retry=True): - """Authenticate user token - - :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 - - """ - token_id = None - - try: - token_ids, cached = self._token_cache.get(user_token) - token_id = token_ids[0] - if cached: - data = cached - - if self.check_revocations_for_cached: - # A token stored in Memcached might have been revoked - # regardless of initial mechanism used to validate it, - # and needs to be checked. - for tid in token_ids: - is_revoked = self._is_token_id_in_revoked_list(tid) - if is_revoked: - self.LOG.debug( - 'Token is marked as having been revoked') - raise InvalidUserToken( - 'Token authorization failed') - elif cms.is_pkiz(user_token): - verified = self.verify_pkiz_token(user_token, token_ids) - data = jsonutils.loads(verified) - elif cms.is_asn1_token(user_token): - verified = self.verify_signed_token(user_token, token_ids) - data = jsonutils.loads(verified) - else: - data = self.verify_uuid_token(user_token, retry) - expires = confirm_token_not_expired(data) - self._confirm_token_bind(data, env) - self._token_cache.store(token_id, data, expires) - return data - except NetworkError: - self.LOG.debug('Token validation failure.', exc_info=True) - self.LOG.warning('Authorization failed for token') - raise InvalidUserToken('Token authorization failed') - except Exception: - self.LOG.debug('Token validation failure.', exc_info=True) - if token_id: - self._token_cache.store_invalid(token_id) - self.LOG.warning('Authorization failed for token') - raise InvalidUserToken('Token authorization failed') - - def _build_user_headers(self, token_info): - """Convert token object into headers. - - Build headers that represent authenticated user - see main - doc info at start of file for details of headers to be defined. - - :param token_info: token object returned by keystone on authentication - :raise InvalidUserToken when unable to parse token object - - """ - auth_ref = access.AccessInfo.factory(body=token_info) - roles = ','.join(auth_ref.role_names) - - if _token_is_v2(token_info) and not auth_ref.project_id: - raise InvalidUserToken('Unable to determine tenancy.') - - rval = { - 'X-Identity-Status': 'Confirmed', - 'X-Domain-Id': auth_ref.domain_id, - 'X-Domain-Name': auth_ref.domain_name, - 'X-Project-Id': auth_ref.project_id, - 'X-Project-Name': auth_ref.project_name, - 'X-Project-Domain-Id': auth_ref.project_domain_id, - 'X-Project-Domain-Name': auth_ref.project_domain_name, - 'X-User-Id': auth_ref.user_id, - 'X-User-Name': auth_ref.username, - 'X-User-Domain-Id': auth_ref.user_domain_id, - 'X-User-Domain-Name': auth_ref.user_domain_name, - 'X-Roles': roles, - # Deprecated - 'X-User': auth_ref.username, - 'X-Tenant-Id': auth_ref.project_id, - 'X-Tenant-Name': auth_ref.project_name, - 'X-Tenant': auth_ref.project_name, - 'X-Role': roles, - } - - self.LOG.debug('Received request from user: %s with project_id : %s' - ' and roles: %s ', - auth_ref.user_id, auth_ref.project_id, roles) - - if self.include_service_catalog and auth_ref.has_service_catalog(): - catalog = auth_ref.service_catalog.get_data() - if _token_is_v3(token_info): - catalog = _v3_to_v2_catalog(catalog) - rval['X-Service-Catalog'] = jsonutils.dumps(catalog) - - return rval - - def _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 six.iteritems(headers): - env_key = self._header_to_env_var(k) - env[env_key] = v - - def _remove_headers(self, env, keys): - """Remove http headers from environment.""" - for k in keys: - env_key = self._header_to_env_var(k) - try: - del env[env_key] - except KeyError: - pass - - def _get_header(self, env, key, default=None): - """Get http header from environment.""" - env_key = self._header_to_env_var(key) - return env.get(env_key, default) - - def _invalid_user_token(self, msg=False): - # NOTE(jamielennox): use False as the default so that None is valid - if msg is False: - msg = 'Token authorization failed' - - raise InvalidUserToken(msg) - - def _confirm_token_bind(self, data, env): - bind_mode = self._conf_get('enforce_token_bind') - - if bind_mode == BIND_MODE.DISABLED: - return - - try: - if _token_is_v2(data): - bind = data['access']['token']['bind'] - elif _token_is_v3(data): - bind = data['token']['bind'] - else: - self._invalid_user_token() - except KeyError: - bind = {} - - # permissive and strict modes don't require there to be a bind - permissive = bind_mode in (BIND_MODE.PERMISSIVE, BIND_MODE.STRICT) - - if not bind: - if permissive: - # no bind provided and none required - return - else: - self.LOG.info('No bind information present in token.') - self._invalid_user_token() - - # get the named mode if bind_mode is not one of the predefined - if permissive or bind_mode == BIND_MODE.REQUIRED: - name = None - else: - name = bind_mode - - if name and name not in bind: - self.LOG.info('Named bind mode %s not in bind information', name) - self._invalid_user_token() - - for bind_type, identifier in six.iteritems(bind): - if bind_type == BIND_MODE.KERBEROS: - if not env.get('AUTH_TYPE', '').lower() == 'negotiate': - self.LOG.info('Kerberos credentials required and ' - 'not present.') - self._invalid_user_token() - - if not env.get('REMOTE_USER') == identifier: - self.LOG.info('Kerberos credentials do not match ' - 'those in bind.') - self._invalid_user_token() - - self.LOG.debug('Kerberos bind authentication successful.') - - elif bind_mode == BIND_MODE.PERMISSIVE: - self.LOG.debug('Ignoring Unknown bind for permissive mode: ' - '%(bind_type)s: %(identifier)s.', - {'bind_type': bind_type, - 'identifier': identifier}) - - else: - self.LOG.info('Couldn`t verify unknown bind: %(bind_type)s: ' - '%(identifier)s.', - {'bind_type': bind_type, - 'identifier': identifier}) - self._invalid_user_token() - - 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. - :returns: token object received from keystone on success - :raise InvalidUserToken: if token is rejected - :raise ServiceError: if unable to authenticate token - - """ - # Determine the highest api version we can use. - if not self.auth_version: - self.auth_version = self._choose_api_version() - - if self.auth_version == 'v3.0': - headers = {'X-Auth-Token': self.get_admin_token(), - 'X-Subject-Token': safe_quote(user_token)} - path = '/v3/auth/tokens' - if not self.include_service_catalog: - # NOTE(gyee): only v3 API support this option - path = path + '?nocatalog' - response, data = self._json_request( - 'GET', - path, - additional_headers=headers) - else: - 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_code == 200: - return data - if response.status_code == 404: - self.LOG.warning('Authorization failed for token') - raise InvalidUserToken('Token authorization failed') - if response.status_code == 401: - self.LOG.info( - 'Keystone rejected admin token, resetting') - self.admin_token = None - else: - self.LOG.error('Bad response code while validating token: %s', - response.status_code) - if retry: - self.LOG.info('Retrying validation') - return self.verify_uuid_token(user_token, False) - else: - self.LOG.warning('Invalid user token. Keystone response: %s', data) - - raise InvalidUserToken() - - def is_signed_token_revoked(self, token_ids): - """Indicate whether the token appears in the revocation list.""" - for token_id in token_ids: - if self._is_token_id_in_revoked_list(token_id): - self.LOG.debug('Token is marked as having been revoked') - return True - return False - - def _is_token_id_in_revoked_list(self, token_id): - """Indicate whether the token_id appears in the revocation list.""" - revocation_list = self.token_revocation_list - revoked_tokens = revocation_list.get('revoked', None) - if not revoked_tokens: - return False - - revoked_ids = (x['id'] for x in revoked_tokens) - return token_id in revoked_ids - - def cms_verify(self, data, inform=cms.PKI_ASN1_FORM): - """Verifies the signature of the provided data's IAW CMS syntax. - - If either of the certificate files might be missing, fetch them and - retry. - """ - def verify(): - try: - return cms.cms_verify(data, self.signing_cert_file_name, - self.signing_ca_file_name, - inform=inform).decode('utf-8') - except cms.subprocess.CalledProcessError as err: - self.LOG.warning('Verify error: %s', err) - raise - - try: - return verify() - except exceptions.CertificateConfigError: - # the certs might be missing; unconditionally fetch to avoid racing - self.fetch_signing_cert() - self.fetch_ca_cert() - - try: - # retry with certs in place - return verify() - except exceptions.CertificateConfigError as err: - # if this is still occurring, something else is wrong and we - # need err.output to identify the problem - self.LOG.error('CMS Verify output: %s', err.output) - raise - - def verify_signed_token(self, signed_text, token_ids): - """Check that the token is unrevoked and has a valid signature.""" - if self.is_signed_token_revoked(token_ids): - raise InvalidUserToken('Token has been revoked') - - formatted = cms.token_to_cms(signed_text) - verified = self.cms_verify(formatted) - return verified - - def verify_pkiz_token(self, signed_text, token_ids): - if self.is_signed_token_revoked(token_ids): - raise InvalidUserToken('Token has been revoked') - try: - uncompressed = cms.pkiz_uncompress(signed_text) - verified = self.cms_verify(uncompressed, inform=cms.PKIZ_CMS_FORM) - return verified - # TypeError If the signed_text is not zlib compressed - except TypeError: - raise InvalidUserToken(signed_text) - - def verify_signing_dir(self): - if os.path.exists(self.signing_dirname): - if not os.access(self.signing_dirname, os.W_OK): - raise ConfigurationError( - 'unable to access signing_dir %s' % self.signing_dirname) - uid = os.getuid() - if os.stat(self.signing_dirname).st_uid != uid: - self.LOG.warning( - 'signing_dir is not owned by %s', uid) - current_mode = stat.S_IMODE(os.stat(self.signing_dirname).st_mode) - if current_mode != stat.S_IRWXU: - self.LOG.warning( - 'signing_dir mode is %s instead of %s', - oct(current_mode), oct(stat.S_IRWXU)) - else: - os.makedirs(self.signing_dirname, stat.S_IRWXU) - - @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.utcfromtimestamp(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: - open_kwargs = {'encoding': 'utf-8'} if six.PY3 else {} - with open(self.revoked_file_name, 'r', **open_kwargs) as f: - self._token_revocation_list = jsonutils.loads(f.read()) - else: - self.token_revocation_list = self.fetch_revocation_list() - return self._token_revocation_list - - def _atomic_write_to_signing_dir(self, file_name, value): - # In Python2, encoding is slow so the following check avoids it if it - # is not absolutely necessary. - if isinstance(value, six.text_type): - value = value.encode('utf-8') - - def _atomic_write(destination, data): - with tempfile.NamedTemporaryFile(dir=self.signing_dirname, - delete=False) as f: - f.write(data) - os.rename(f.name, destination) - - try: - _atomic_write(file_name, value) - except (OSError, IOError): - self.verify_signing_dir() - _atomic_write(file_name, value) - - @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() - self._atomic_write_to_signing_dir(self.revoked_file_name, 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_code == 401: - if retry: - self.LOG.info( - 'Keystone rejected admin token, resetting admin token') - self.admin_token = None - return self.fetch_revocation_list(retry=False) - if response.status_code != 200: - raise ServiceError('Unable to fetch token revocation list.') - if 'signed' not in data: - raise ServiceError('Revocation list improperly formatted.') - return self.cms_verify(data['signed']) - - def _fetch_cert_file(self, cert_file_name, cert_type): - if not self.auth_version: - self.auth_version = self._choose_api_version() - - if self.auth_version == 'v3.0': - if cert_type == 'signing': - cert_type = 'certificates' - path = '/v3/OS-SIMPLE-CERT/' + cert_type - else: - path = '/v2.0/certificates/' + cert_type - response = self._http_request('GET', path) - if response.status_code != 200: - raise exceptions.CertificateConfigError(response.text) - self._atomic_write_to_signing_dir(cert_file_name, response.text) - - def fetch_signing_cert(self): - self._fetch_cert_file(self.signing_cert_file_name, 'signing') - - def fetch_ca_cert(self): - self._fetch_cert_file(self.signing_ca_file_name, 'ca') - - -class CachePool(list): - """A lazy pool of cache references.""" - - def __init__(self, cache, memcached_servers): - self._environment_cache = cache - self._memcached_servers = memcached_servers - - @contextlib.contextmanager - def reserve(self): - """Context manager to manage a pooled cache reference.""" - if self._environment_cache is not None: - # skip pooling and just use the cache from the upstream filter - yield self._environment_cache - return # otherwise the context manager will continue! - - try: - c = self.pop() - except IndexError: - # the pool is empty, so we need to create a new client - c = memorycache.get_client(self._memcached_servers) - - try: - yield c - finally: - self.append(c) - - -class TokenCache(object): - """Encapsulates the auth_token token cache functionality. - - auth_token caches tokens that it's seen so that when a token is re-used the - middleware doesn't have to do a more expensive operation (like going to the - identity server) to validate the token. - - initialize() must be called before calling the other methods. - - Store a valid token in the cache using store(); mark a token as invalid in - the cache using store_invalid(). - - Check if a token is in the cache and retrieve it using get(). - - """ - - _INVALID_INDICATOR = 'invalid' - - def __init__(self, log, cache_time=None, hash_algorithms=None, - env_cache_name=None, memcached_servers=None, - memcache_security_strategy=None, memcache_secret_key=None): - self.LOG = log - self._cache_time = cache_time - self._hash_algorithms = hash_algorithms - self._env_cache_name = env_cache_name - self._memcached_servers = memcached_servers - - # memcache value treatment, ENCRYPT or MAC - self._memcache_security_strategy = memcache_security_strategy - if self._memcache_security_strategy is not None: - self._memcache_security_strategy = ( - self._memcache_security_strategy.upper()) - self._memcache_secret_key = memcache_secret_key - - self._cache_pool = None - self._initialized = False - - self._assert_valid_memcache_protection_config() - - def initialize(self, env): - if self._initialized: - return - - self._cache_pool = CachePool(env.get(self._env_cache_name), - self._memcached_servers) - self._initialized = True - - def get(self, user_token): - """Check if the token is cached already. - - Returns a tuple. The first element is a list of token IDs, where the - first one is the preferred hash. - - The second element is the token data from the cache if the token was - cached, otherwise ``None``. - - :raises InvalidUserToken: if the token is invalid - - """ - - if cms.is_asn1_token(user_token) or cms.is_pkiz(user_token): - # user_token is a PKI token that's not hashed. - - token_hashes = list(cms.cms_hash_token(user_token, mode=algo) - for algo in self._hash_algorithms) - - for token_hash in token_hashes: - cached = self._cache_get(token_hash) - if cached: - return (token_hashes, cached) - - # The token wasn't found using any hash algorithm. - return (token_hashes, None) - - # user_token is either a UUID token or a hashed PKI token. - token_id = user_token - cached = self._cache_get(token_id) - return ([token_id], cached) - - def store(self, token_id, data, expires): - """Put token data into the cache. - - Stores the parsed expire date in cache allowing - quick check of token freshness on retrieval. - - """ - self.LOG.debug('Storing token in cache') - self._cache_store(token_id, (data, expires)) - - def store_invalid(self, token_id): - """Store invalid token in cache.""" - self.LOG.debug('Marking token as unauthorized in cache') - self._cache_store(token_id, self._INVALID_INDICATOR) - - def _assert_valid_memcache_protection_config(self): - if self._memcache_security_strategy: - if self._memcache_security_strategy not in ('MAC', 'ENCRYPT'): - raise ConfigurationError('memcache_security_strategy must be ' - 'ENCRYPT or MAC') - if not self._memcache_secret_key: - raise ConfigurationError('memcache_secret_key must be defined ' - 'when a memcache_security_strategy ' - 'is defined') - - def _cache_get(self, token_id): - """Return token information from cache. - - If token is invalid raise InvalidUserToken - return token only if fresh (not expired). - """ - - if not token_id: - # Nothing to do - return - - if self._memcache_security_strategy is None: - key = CACHE_KEY_TEMPLATE % token_id - with self._cache_pool.reserve() as cache: - serialized = cache.get(key) - else: - secret_key = self._memcache_secret_key - if isinstance(secret_key, six.string_types): - secret_key = secret_key.encode('utf-8') - security_strategy = self._memcache_security_strategy - if isinstance(security_strategy, six.string_types): - security_strategy = security_strategy.encode('utf-8') - keys = memcache_crypt.derive_keys( - token_id, - secret_key, - security_strategy) - cache_key = CACHE_KEY_TEMPLATE % ( - memcache_crypt.get_cache_key(keys)) - with self._cache_pool.reserve() as cache: - raw_cached = cache.get(cache_key) - try: - # unprotect_data will return None if raw_cached is None - serialized = memcache_crypt.unprotect_data(keys, - raw_cached) - except Exception: - msg = 'Failed to decrypt/verify cache data' - self.LOG.exception(msg) - # this should have the same effect as data not - # found in cache - serialized = None - - if serialized is None: - return None - - # Note that _INVALID_INDICATOR and (data, expires) are the only - # valid types of serialized cache entries, so there is not - # a collision with jsonutils.loads(serialized) == None. - if not isinstance(serialized, six.string_types): - serialized = serialized.decode('utf-8') - cached = jsonutils.loads(serialized) - if cached == self._INVALID_INDICATOR: - self.LOG.debug('Cached Token is marked unauthorized') - raise InvalidUserToken('Token authorization failed') - - data, expires = cached - - try: - expires = timeutils.parse_isotime(expires) - except ValueError: - # Gracefully handle upgrade of expiration times from *nix - # timestamps to ISO 8601 formatted dates by ignoring old cached - # values. - return - - expires = timeutils.normalize_time(expires) - utcnow = timeutils.utcnow() - if utcnow < expires: - self.LOG.debug('Returning cached token') - return data - else: - self.LOG.debug('Cached Token seems expired') - raise InvalidUserToken('Token authorization failed') - - def _cache_store(self, token_id, data): - """Store value into memcache. - - data may be _INVALID_INDICATOR or a tuple like (data, expires) - - """ - serialized_data = jsonutils.dumps(data) - if isinstance(serialized_data, six.text_type): - serialized_data = serialized_data.encode('utf-8') - if self._memcache_security_strategy is None: - cache_key = CACHE_KEY_TEMPLATE % token_id - data_to_store = serialized_data - else: - secret_key = self._memcache_secret_key - if isinstance(secret_key, six.string_types): - secret_key = secret_key.encode('utf-8') - security_strategy = self._memcache_security_strategy - if isinstance(security_strategy, six.string_types): - security_strategy = security_strategy.encode('utf-8') - keys = memcache_crypt.derive_keys( - token_id, secret_key, security_strategy) - cache_key = CACHE_KEY_TEMPLATE % memcache_crypt.get_cache_key(keys) - data_to_store = memcache_crypt.protect_data(keys, serialized_data) - - with self._cache_pool.reserve() as cache: - cache.set(cache_key, data_to_store, time=self._cache_time) - - -def filter_factory(global_conf, **local_conf): - """Returns a WSGI filter app for use with paste.deploy.""" - conf = global_conf.copy() - conf.update(local_conf) - - def auth_filter(app): - return AuthProtocol(app, conf) - return auth_filter - - -def app_factory(global_conf, **local_conf): - conf = global_conf.copy() - conf.update(local_conf) - return AuthProtocol(None, conf) - - -if __name__ == '__main__': - """Run this module directly to start a protected echo service:: - - $ python -m keystoneclient.middleware.auth_token - - When the ``auth_token`` module authenticates a request, the echo service - will respond with all the environment variables presented to it by this - module. - - """ - def echo_app(environ, start_response): - """A WSGI application that echoes the CGI environment to the user.""" - start_response('200 OK', [('Content-Type', 'application/json')]) - environment = dict((k, v) for k, v in six.iteritems(environ) - if k.startswith('HTTP_X_')) - yield jsonutils.dumps(environment) - - from wsgiref import simple_server - - # hardcode any non-default configuration here - conf = {'auth_protocol': 'http', 'admin_token': 'ADMIN'} - app = AuthProtocol(echo_app, conf) - server = simple_server.make_server('', 8000, app) - print('Serving on port 8000 (Ctrl+C to end)...') - server.serve_forever() diff --git a/keystoneclient/middleware/memcache_crypt.py b/keystoneclient/middleware/memcache_crypt.py deleted file mode 100644 index 40e205132..000000000 --- a/keystoneclient/middleware/memcache_crypt.py +++ /dev/null @@ -1,209 +0,0 @@ -# Copyright 2010-2013 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Utilities for memcache encryption and integrity check. - -Data should be serialized before entering these functions. Encryption -has a dependency on the pycrypto. If pycrypto is not available, -CryptoUnavailableError will be raised. - -This module will not be called unless signing or encryption is enabled -in the config. It will always validate signatures, and will decrypt -data if encryption is enabled. It is not valid to mix protection -modes. - -""" - -import base64 -import functools -import hashlib -import hmac -import math -import os -import sys - -import six - -# make sure pycrypto is available -try: - from Crypto.Cipher import AES -except ImportError: - AES = None - -HASH_FUNCTION = hashlib.sha384 -DIGEST_LENGTH = HASH_FUNCTION().digest_size -DIGEST_SPLIT = DIGEST_LENGTH // 3 -DIGEST_LENGTH_B64 = 4 * int(math.ceil(DIGEST_LENGTH / 3.0)) - - -class InvalidMacError(Exception): - """raise when unable to verify MACed data. - - This usually indicates that data had been expectedly modified in memcache. - - """ - pass - - -class DecryptError(Exception): - """raise when unable to decrypt encrypted data. - - """ - pass - - -class CryptoUnavailableError(Exception): - """raise when Python Crypto module is not available. - - """ - pass - - -def assert_crypto_availability(f): - """Ensure Crypto module is available.""" - - @functools.wraps(f) - def wrapper(*args, **kwds): - if AES is None: - raise CryptoUnavailableError() - return f(*args, **kwds) - return wrapper - - -if sys.version_info >= (3, 3): - constant_time_compare = hmac.compare_digest -else: - def constant_time_compare(first, second): - """Returns True if both string inputs are equal, otherwise False. - - This function should take a constant amount of time regardless of - how many characters in the strings match. - - """ - if len(first) != len(second): - return False - result = 0 - if six.PY3 and isinstance(first, bytes) and isinstance(second, bytes): - for x, y in zip(first, second): - result |= x ^ y - else: - for x, y in zip(first, second): - result |= ord(x) ^ ord(y) - return result == 0 - - -def derive_keys(token, secret, strategy): - """Derives keys for MAC and ENCRYPTION from the user-provided - secret. The resulting keys should be passed to the protect and - unprotect functions. - - As suggested by NIST Special Publication 800-108, this uses the - first 128 bits from the sha384 KDF for the obscured cache key - value, the second 128 bits for the message authentication key and - the remaining 128 bits for the encryption key. - - This approach is faster than computing a separate hmac as the KDF - for each desired key. - """ - digest = hmac.new(secret, token + strategy, HASH_FUNCTION).digest() - return {'CACHE_KEY': digest[:DIGEST_SPLIT], - 'MAC': digest[DIGEST_SPLIT: 2 * DIGEST_SPLIT], - 'ENCRYPTION': digest[2 * DIGEST_SPLIT:], - 'strategy': strategy} - - -def sign_data(key, data): - """Sign the data using the defined function and the derived key.""" - mac = hmac.new(key, data, HASH_FUNCTION).digest() - return base64.b64encode(mac) - - -@assert_crypto_availability -def encrypt_data(key, data): - """Encrypt the data with the given secret key. - - Padding is n bytes of the value n, where 1 <= n <= blocksize. - """ - iv = os.urandom(16) - cipher = AES.new(key, AES.MODE_CBC, iv) - padding = 16 - len(data) % 16 - return iv + cipher.encrypt(data + six.int2byte(padding) * padding) - - -@assert_crypto_availability -def decrypt_data(key, data): - """Decrypt the data with the given secret key.""" - iv = data[:16] - cipher = AES.new(key, AES.MODE_CBC, iv) - try: - result = cipher.decrypt(data[16:]) - except Exception: - raise DecryptError('Encrypted data appears to be corrupted.') - - # Strip the last n padding bytes where n is the last value in - # the plaintext - return result[:-1 * six.byte2int([result[-1]])] - - -def protect_data(keys, data): - """Given keys and serialized data, returns an appropriately - protected string suitable for storage in the cache. - - """ - if keys['strategy'] == b'ENCRYPT': - data = encrypt_data(keys['ENCRYPTION'], data) - - encoded_data = base64.b64encode(data) - - signature = sign_data(keys['MAC'], encoded_data) - return signature + encoded_data - - -def unprotect_data(keys, signed_data): - """Given keys and cached string data, verifies the signature, - decrypts if necessary, and returns the original serialized data. - - """ - # cache backends return None when no data is found. We don't mind - # that this particular special value is unsigned. - if signed_data is None: - return None - - # First we calculate the signature - provided_mac = signed_data[:DIGEST_LENGTH_B64] - calculated_mac = sign_data( - keys['MAC'], - signed_data[DIGEST_LENGTH_B64:]) - - # Then verify that it matches the provided value - if not constant_time_compare(provided_mac, calculated_mac): - raise InvalidMacError('Invalid MAC; data appears to be corrupted.') - - data = base64.b64decode(signed_data[DIGEST_LENGTH_B64:]) - - # then if necessary decrypt the data - if keys['strategy'] == b'ENCRYPT': - data = decrypt_data(keys['ENCRYPTION'], data) - - return data - - -def get_cache_key(keys): - """Given keys generated by derive_keys(), returns a base64 - encoded value suitable for use as a cache key in memcached. - - """ - return base64.b64encode(keys['CACHE_KEY']) diff --git a/keystoneclient/middleware/s3_token.py b/keystoneclient/middleware/s3_token.py deleted file mode 100644 index f8d1ce0bf..000000000 --- a/keystoneclient/middleware/s3_token.py +++ /dev/null @@ -1,268 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011,2012 Akira YOSHIYAMA -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# This source code is based ./auth_token.py and ./ec2_token.py. -# See them for their copyright. - -""" -S3 TOKEN MIDDLEWARE - -This WSGI component: - -* Get a request from the swift3 middleware with an S3 Authorization - access key. -* Validate s3 token in Keystone. -* Transform the account name to AUTH_%(tenant_name). - -""" - -import logging - -from oslo_serialization import jsonutils -from oslo_utils import strutils -import requests -import six -from six.moves import urllib -import webob - - -PROTOCOL_NAME = 'S3 Token Authentication' - - -# TODO(kun): remove it after oslo merge this. -def split_path(path, minsegs=1, maxsegs=None, rest_with_last=False): - """Validate and split the given HTTP request path. - - **Examples**:: - - ['a'] = split_path('/a') - ['a', None] = split_path('/a', 1, 2) - ['a', 'c'] = split_path('/a/c', 1, 2) - ['a', 'c', 'o/r'] = split_path('/a/c/o/r', 1, 3, True) - - :param path: HTTP Request path to be split - :param minsegs: Minimum number of segments to be extracted - :param maxsegs: Maximum number of segments to be extracted - :param rest_with_last: If True, trailing data will be returned as part - of last segment. If False, and there is - trailing data, raises ValueError. - :returns: list of segments with a length of maxsegs (non-existent - segments will return as None) - :raises: ValueError if given an invalid path - """ - if not maxsegs: - maxsegs = minsegs - if minsegs > maxsegs: - raise ValueError('minsegs > maxsegs: %d > %d' % (minsegs, maxsegs)) - if rest_with_last: - segs = path.split('/', maxsegs) - minsegs += 1 - maxsegs += 1 - count = len(segs) - if (segs[0] or count < minsegs or count > maxsegs or - '' in segs[1:minsegs]): - raise ValueError('Invalid path: %s' % urllib.parse.quote(path)) - else: - minsegs += 1 - maxsegs += 1 - segs = path.split('/', maxsegs) - count = len(segs) - if (segs[0] or count < minsegs or count > maxsegs + 1 or - '' in segs[1:minsegs] or - (count == maxsegs + 1 and segs[maxsegs])): - raise ValueError('Invalid path: %s' % urllib.parse.quote(path)) - segs = segs[1:maxsegs] - segs.extend([None] * (maxsegs - 1 - len(segs))) - return segs - - -class ServiceError(Exception): - pass - - -class S3Token(object): - """Auth Middleware that handles S3 authenticating client calls.""" - - def __init__(self, app, conf): - """Common initialization code.""" - self.app = app - self.logger = logging.getLogger(conf.get('log_name', __name__)) - self.logger.debug('Starting the %s component', PROTOCOL_NAME) - self.logger.warning( - 'This middleware module is deprecated as of v0.11.0 in favor of ' - 'keystonemiddleware.s3_token - please update your WSGI pipeline ' - 'to reference the new middleware package.') - self.reseller_prefix = conf.get('reseller_prefix', 'AUTH_') - # where to find the auth service (we use this to validate tokens) - - auth_host = conf.get('auth_host') - auth_port = int(conf.get('auth_port', 35357)) - auth_protocol = conf.get('auth_protocol', 'https') - - self.request_uri = '%s://%s:%s' % (auth_protocol, auth_host, auth_port) - - # SSL - insecure = strutils.bool_from_string(conf.get('insecure', False)) - cert_file = conf.get('certfile') - key_file = conf.get('keyfile') - - if insecure: - self.verify = False - elif cert_file and key_file: - self.verify = (cert_file, key_file) - elif cert_file: - self.verify = cert_file - else: - self.verify = None - - def deny_request(self, code): - error_table = { - 'AccessDenied': (401, 'Access denied'), - 'InvalidURI': (400, 'Could not parse the specified URI'), - } - resp = webob.Response(content_type='text/xml') - resp.status = error_table[code][0] - error_msg = ('\r\n' - '\r\n %s\r\n ' - '%s\r\n\r\n' % - (code, error_table[code][1])) - if six.PY3: - error_msg = error_msg.encode() - resp.body = error_msg - return resp - - def _json_request(self, creds_json): - headers = {'Content-Type': 'application/json'} - try: - response = requests.post('%s/v2.0/s3tokens' % self.request_uri, - headers=headers, data=creds_json, - verify=self.verify) - except requests.exceptions.RequestException as e: - self.logger.info('HTTP connection exception: %s', e) - resp = self.deny_request('InvalidURI') - raise ServiceError(resp) - - if response.status_code < 200 or response.status_code >= 300: - self.logger.debug('Keystone reply error: status=%s reason=%s', - response.status_code, response.reason) - resp = self.deny_request('AccessDenied') - raise ServiceError(resp) - - return response - - def __call__(self, environ, start_response): - """Handle incoming request. authenticate and send downstream.""" - req = webob.Request(environ) - self.logger.debug('Calling S3Token middleware.') - - try: - parts = split_path(req.path, 1, 4, True) - version, account, container, obj = parts - except ValueError: - msg = 'Not a path query, skipping.' - self.logger.debug(msg) - return self.app(environ, start_response) - - # Read request signature and access id. - if 'Authorization' not in req.headers: - msg = 'No Authorization header. skipping.' - self.logger.debug(msg) - return self.app(environ, start_response) - - token = req.headers.get('X-Auth-Token', - req.headers.get('X-Storage-Token')) - if not token: - msg = 'You did not specify an auth or a storage token. skipping.' - self.logger.debug(msg) - return self.app(environ, start_response) - - auth_header = req.headers['Authorization'] - try: - access, signature = auth_header.split(' ')[-1].rsplit(':', 1) - except ValueError: - msg = 'You have an invalid Authorization header: %s' - self.logger.debug(msg, auth_header) - return self.deny_request('InvalidURI')(environ, start_response) - - # NOTE(chmou): This is to handle the special case with nova - # when we have the option s3_affix_tenant. We will force it to - # connect to another account than the one - # authenticated. Before people start getting worried about - # security, I should point that we are connecting with - # username/token specified by the user but instead of - # connecting to its own account we will force it to go to an - # another account. In a normal scenario if that user don't - # have the reseller right it will just fail but since the - # reseller account can connect to every account it is allowed - # by the swift_auth middleware. - force_tenant = None - if ':' in access: - access, force_tenant = access.split(':') - - # Authenticate request. - creds = {'credentials': {'access': access, - 'token': token, - 'signature': signature}} - creds_json = jsonutils.dumps(creds) - self.logger.debug('Connecting to Keystone sending this JSON: %s', - creds_json) - # NOTE(vish): We could save a call to keystone by having - # keystone return token, tenant, user, and roles - # from this call. - # - # NOTE(chmou): We still have the same problem we would need to - # change token_auth to detect if we already - # identified and not doing a second query and just - # pass it through to swiftauth in this case. - try: - resp = self._json_request(creds_json) - except ServiceError as e: - resp = e.args[0] - msg = 'Received error, exiting middleware with error: %s' - self.logger.debug(msg, resp.status_code) - return resp(environ, start_response) - - self.logger.debug('Keystone Reply: Status: %d, Output: %s', - resp.status_code, resp.content) - - try: - identity_info = resp.json() - token_id = str(identity_info['access']['token']['id']) - tenant = identity_info['access']['token']['tenant'] - except (ValueError, KeyError): - error = 'Error on keystone reply: %d %s' - self.logger.debug(error, resp.status_code, resp.content) - return self.deny_request('InvalidURI')(environ, start_response) - - req.headers['X-Auth-Token'] = token_id - tenant_to_connect = force_tenant or tenant['id'] - self.logger.debug('Connecting with tenant: %s', tenant_to_connect) - new_tenant_name = '%s%s' % (self.reseller_prefix, tenant_to_connect) - environ['PATH_INFO'] = environ['PATH_INFO'].replace(account, - new_tenant_name) - return self.app(environ, start_response) - - -def filter_factory(global_conf, **local_conf): - """Returns a WSGI filter app for use with paste.deploy.""" - conf = global_conf.copy() - conf.update(local_conf) - - def auth_filter(app): - return S3Token(app, conf) - return auth_filter diff --git a/keystoneclient/openstack/__init__.py b/keystoneclient/openstack/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/keystoneclient/openstack/common/__init__.py b/keystoneclient/openstack/common/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/keystoneclient/openstack/common/_i18n.py b/keystoneclient/openstack/common/_i18n.py deleted file mode 100644 index b4275359a..000000000 --- a/keystoneclient/openstack/common/_i18n.py +++ /dev/null @@ -1,45 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""oslo.i18n integration module. - -See http://docs.openstack.org/developer/oslo.i18n/usage.html - -""" - -try: - import oslo_i18n - - # NOTE(dhellmann): This reference to o-s-l-o will be replaced by the - # application name when this module is synced into the separate - # repository. It is OK to have more than one translation function - # using the same domain, since there will still only be one message - # catalog. - _translators = oslo_i18n.TranslatorFactory(domain='keystoneclient') - - # The primary translation function using the well-known name "_" - _ = _translators.primary - - # Translators for log levels. - # - # The abbreviated names are meant to reflect the usual use of a short - # name like '_'. The "L" is for "log" and the other letter comes from - # the level. - _LI = _translators.log_info - _LW = _translators.log_warning - _LE = _translators.log_error - _LC = _translators.log_critical -except ImportError: - # NOTE(dims): Support for cases where a project wants to use - # code from oslo-incubator, but is not ready to be internationalized - # (like tempest) - _ = _LI = _LW = _LE = _LC = lambda x: x diff --git a/keystoneclient/openstack/common/apiclient/__init__.py b/keystoneclient/openstack/common/apiclient/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/keystoneclient/openstack/common/apiclient/auth.py b/keystoneclient/openstack/common/apiclient/auth.py deleted file mode 100644 index 003c086e1..000000000 --- a/keystoneclient/openstack/common/apiclient/auth.py +++ /dev/null @@ -1,234 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# Copyright 2013 Spanish National Research Council. -# 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. - -# E0202: An attribute inherited from %s hide this method -# pylint: disable=E0202 - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-keystoneclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - -import abc -import argparse -import os - -import six -from stevedore import extension - -from keystoneclient.openstack.common.apiclient import exceptions - - -_discovered_plugins = {} - - -def discover_auth_systems(): - """Discover the available auth-systems. - - This won't take into account the old style auth-systems. - """ - global _discovered_plugins - _discovered_plugins = {} - - def add_plugin(ext): - _discovered_plugins[ext.name] = ext.plugin - - ep_namespace = "keystoneclient.openstack.common.apiclient.auth" - mgr = extension.ExtensionManager(ep_namespace) - mgr.map(add_plugin) - - -def load_auth_system_opts(parser): - """Load options needed by the available auth-systems into a parser. - - This function will try to populate the parser with options from the - available plugins. - """ - group = parser.add_argument_group("Common auth options") - BaseAuthPlugin.add_common_opts(group) - for name, auth_plugin in six.iteritems(_discovered_plugins): - group = parser.add_argument_group( - "Auth-system '%s' options" % name, - conflict_handler="resolve") - auth_plugin.add_opts(group) - - -def load_plugin(auth_system): - try: - plugin_class = _discovered_plugins[auth_system] - except KeyError: - raise exceptions.AuthSystemNotFound(auth_system) - return plugin_class(auth_system=auth_system) - - -def load_plugin_from_args(args): - """Load required plugin and populate it with options. - - Try to guess auth system if it is not specified. Systems are tried in - alphabetical order. - - :type args: argparse.Namespace - :raises: AuthPluginOptionsMissing - """ - auth_system = args.os_auth_system - if auth_system: - plugin = load_plugin(auth_system) - plugin.parse_opts(args) - plugin.sufficient_options() - return plugin - - for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)): - plugin_class = _discovered_plugins[plugin_auth_system] - plugin = plugin_class() - plugin.parse_opts(args) - try: - plugin.sufficient_options() - except exceptions.AuthPluginOptionsMissing: - continue - return plugin - raise exceptions.AuthPluginOptionsMissing(["auth_system"]) - - -@six.add_metaclass(abc.ABCMeta) -class BaseAuthPlugin(object): - """Base class for authentication plugins. - - An authentication plugin needs to override at least the authenticate - method to be a valid plugin. - """ - - auth_system = None - opt_names = [] - common_opt_names = [ - "auth_system", - "username", - "password", - "tenant_name", - "token", - "auth_url", - ] - - def __init__(self, auth_system=None, **kwargs): - self.auth_system = auth_system or self.auth_system - self.opts = dict((name, kwargs.get(name)) - for name in self.opt_names) - - @staticmethod - def _parser_add_opt(parser, opt): - """Add an option to parser in two variants. - - :param opt: option name (with underscores) - """ - dashed_opt = opt.replace("_", "-") - env_var = "OS_%s" % opt.upper() - arg_default = os.environ.get(env_var, "") - arg_help = "Defaults to env[%s]." % env_var - parser.add_argument( - "--os-%s" % dashed_opt, - metavar="<%s>" % dashed_opt, - default=arg_default, - help=arg_help) - parser.add_argument( - "--os_%s" % opt, - metavar="<%s>" % dashed_opt, - help=argparse.SUPPRESS) - - @classmethod - def add_opts(cls, parser): - """Populate the parser with the options for this plugin. - """ - for opt in cls.opt_names: - # use `BaseAuthPlugin.common_opt_names` since it is never - # changed in child classes - if opt not in BaseAuthPlugin.common_opt_names: - cls._parser_add_opt(parser, opt) - - @classmethod - def add_common_opts(cls, parser): - """Add options that are common for several plugins. - """ - for opt in cls.common_opt_names: - cls._parser_add_opt(parser, opt) - - @staticmethod - def get_opt(opt_name, args): - """Return option name and value. - - :param opt_name: name of the option, e.g., "username" - :param args: parsed arguments - """ - return (opt_name, getattr(args, "os_%s" % opt_name, None)) - - def parse_opts(self, args): - """Parse the actual auth-system options if any. - - This method is expected to populate the attribute `self.opts` with a - dict containing the options and values needed to make authentication. - """ - self.opts.update(dict(self.get_opt(opt_name, args) - for opt_name in self.opt_names)) - - def authenticate(self, http_client): - """Authenticate using plugin defined method. - - The method usually analyses `self.opts` and performs - a request to authentication server. - - :param http_client: client object that needs authentication - :type http_client: HTTPClient - :raises: AuthorizationFailure - """ - self.sufficient_options() - self._do_authenticate(http_client) - - @abc.abstractmethod - def _do_authenticate(self, http_client): - """Protected method for authentication. - """ - - def sufficient_options(self): - """Check if all required options are present. - - :raises: AuthPluginOptionsMissing - """ - missing = [opt - for opt in self.opt_names - if not self.opts.get(opt)] - if missing: - raise exceptions.AuthPluginOptionsMissing(missing) - - @abc.abstractmethod - def token_and_endpoint(self, endpoint_type, service_type): - """Return token and endpoint. - - :param service_type: Service type of the endpoint - :type service_type: string - :param endpoint_type: Type of endpoint. - Possible values: public or publicURL, - internal or internalURL, - admin or adminURL - :type endpoint_type: string - :returns: tuple of token and endpoint strings - :raises: EndpointException - """ diff --git a/keystoneclient/openstack/common/apiclient/base.py b/keystoneclient/openstack/common/apiclient/base.py deleted file mode 100644 index 9300c2e7f..000000000 --- a/keystoneclient/openstack/common/apiclient/base.py +++ /dev/null @@ -1,532 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack Foundation -# Copyright 2012 Grid Dynamics -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Base utilities to build API operation managers and objects on top of. -""" - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-keystoneclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - - -# E1102: %s is not callable -# pylint: disable=E1102 - -import abc -import copy - -from oslo_utils import strutils -import six -from six.moves.urllib import parse - -from keystoneclient.openstack.common._i18n import _ -from keystoneclient.openstack.common.apiclient import exceptions - - -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. - """ - try: - if obj.uuid: - return obj.uuid - except AttributeError: - pass - try: - return obj.id - except AttributeError: - return obj - - -# TODO(aababilov): call run_hooks() in HookableMixin's child classes -class HookableMixin(object): - """Mixin so classes can register and run hooks.""" - _hooks_map = {} - - @classmethod - def add_hook(cls, hook_type, hook_func): - """Add a new hook of specified type. - - :param cls: class that registers hooks - :param hook_type: hook type, e.g., '__pre_parse_args__' - :param hook_func: hook function - """ - if hook_type not in cls._hooks_map: - cls._hooks_map[hook_type] = [] - - cls._hooks_map[hook_type].append(hook_func) - - @classmethod - def run_hooks(cls, hook_type, *args, **kwargs): - """Run all hooks of specified type. - - :param cls: class that registers hooks - :param hook_type: hook type, e.g., '__pre_parse_args__' - :param args: args to be passed to every hook function - :param kwargs: kwargs to be passed to every hook function - """ - hook_funcs = cls._hooks_map.get(hook_type) or [] - for hook_func in hook_funcs: - hook_func(*args, **kwargs) - - -class BaseManager(HookableMixin): - """Basic manager type providing common operations. - - Managers interact with a particular type of API (servers, flavors, images, - etc.) and provide CRUD operations for them. - """ - resource_class = None - - def __init__(self, client): - """Initializes BaseManager with `client`. - - :param client: instance of BaseClient descendant for HTTP requests - """ - super(BaseManager, self).__init__() - self.client = client - - def _list(self, url, response_key=None, obj_class=None, json=None): - """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'. If response_key is None - all response body - will be used. - :param obj_class: class for constructing the returned objects - (self.resource_class will be used by default) - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - """ - if json: - body = self.client.post(url, json=json).json() - else: - body = self.client.get(url).json() - - if obj_class is None: - obj_class = self.resource_class - - data = body[response_key] if response_key is not None else body - # NOTE(ja): keystone returns values as list as {'values': [ ... ]} - # unlike other services which just return the list... - try: - data = data['values'] - except (KeyError, TypeError): - pass - - return [obj_class(self, res, loaded=True) for res in data if res] - - def _get(self, url, response_key=None): - """Get an object from collection. - - :param url: a partial URL, e.g., '/servers' - :param response_key: the key to be looked up in response dictionary, - e.g., 'server'. If response_key is None - all response body - will be used. - """ - body = self.client.get(url).json() - data = body[response_key] if response_key is not None else body - return self.resource_class(self, data, loaded=True) - - def _head(self, url): - """Retrieve request headers for an object. - - :param url: a partial URL, e.g., '/servers' - """ - resp = self.client.head(url) - return resp.status_code == 204 - - def _post(self, url, json, response_key=None, return_raw=False): - """Create an object. - - :param url: a partial URL, e.g., '/servers' - :param json: 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., 'server'. If response_key is None - all response body - will be used. - :param return_raw: flag to force returning raw JSON instead of - Python object of self.resource_class - """ - body = self.client.post(url, json=json).json() - data = body[response_key] if response_key is not None else body - if return_raw: - return data - return self.resource_class(self, data) - - def _put(self, url, json=None, response_key=None): - """Update an object with PUT method. - - :param url: a partial URL, e.g., '/servers' - :param json: 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'. If response_key is None - all response body - will be used. - """ - resp = self.client.put(url, json=json) - # PUT requests may not return a body - if resp.content: - body = resp.json() - if response_key is not None: - return self.resource_class(self, body[response_key]) - else: - return self.resource_class(self, body) - - def _patch(self, url, json=None, response_key=None): - """Update an object with PATCH method. - - :param url: a partial URL, e.g., '/servers' - :param json: 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'. If response_key is None - all response body - will be used. - """ - body = self.client.patch(url, json=json).json() - if response_key is not None: - return self.resource_class(self, body[response_key]) - else: - return self.resource_class(self, body) - - def _delete(self, url): - """Delete an object. - - :param url: a partial URL, e.g., '/servers/my-server' - """ - return self.client.delete(url) - - -@six.add_metaclass(abc.ABCMeta) -class ManagerWithFind(BaseManager): - """Manager with additional `find()`/`findall()` methods.""" - - @abc.abstractmethod - def list(self): - pass - - def find(self, **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. - """ - matches = self.findall(**kwargs) - num_matches = len(matches) - if num_matches == 0: - msg = _("No %(name)s matching %(args)s.") % { - 'name': self.resource_class.__name__, - 'args': kwargs - } - raise exceptions.NotFound(msg) - elif num_matches > 1: - raise exceptions.NoUniqueMatch() - else: - return matches[0] - - def findall(self, **kwargs): - """Find all items with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ - 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 - - return found - - -class CrudManager(BaseManager): - """Base manager class for manipulating entities. - - Children of this class are expected to define a `collection_key` and `key`. - - - `collection_key`: Usually a plural noun by convention (e.g. `entities`); - used to refer collections in both URL's (e.g. `/v3/entities`) and JSON - objects containing a list of member resources (e.g. `{'entities': [{}, - {}, {}]}`). - - `key`: Usually a singular noun by convention (e.g. `entity`); used to - refer to an individual member of the collection. - - """ - collection_key = None - key = None - - def build_url(self, base_url=None, **kwargs): - """Builds 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. - - By default, the URL will represent a collection of entities, e.g.:: - - /entities - - If kwargs contains an `entity_id`, then the URL will represent a - specific member, e.g.:: - - /entities/{entity_id} - - :param base_url: if provided, the generated URL will be appended to it - """ - url = base_url if base_url is not None else '' - - url += '/%s' % self.collection_key - - # do we have a specific entity? - entity_id = kwargs.get('%s_id' % self.key) - if entity_id is not None: - url += '/%s' % entity_id - - return url - - def _filter_kwargs(self, kwargs): - """Drop null values and handle ids.""" - for key, ref in six.iteritems(kwargs.copy()): - if ref is None: - kwargs.pop(key) - else: - if isinstance(ref, Resource): - kwargs.pop(key) - kwargs['%s_id' % key] = getid(ref) - return kwargs - - def create(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._post( - self.build_url(**kwargs), - {self.key: kwargs}, - self.key) - - def get(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._get( - self.build_url(**kwargs), - self.key) - - def head(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._head(self.build_url(**kwargs)) - - def list(self, base_url=None, **kwargs): - """List the collection. - - :param base_url: if provided, the generated URL will be appended to it - """ - 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' % parse.urlencode(kwargs) if kwargs else '', - }, - self.collection_key) - - def put(self, base_url=None, **kwargs): - """Update an element. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - return self._put(self.build_url(base_url=base_url, **kwargs)) - - def update(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - params = kwargs.copy() - params.pop('%s_id' % self.key) - - return self._patch( - self.build_url(**kwargs), - {self.key: params}, - self.key) - - def delete(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - - return self._delete( - self.build_url(**kwargs)) - - def find(self, base_url=None, **kwargs): - """Find a single item with attributes matching ``**kwargs``. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - rl = self._list( - '%(base_url)s%(query)s' % { - 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', - }, - self.collection_key) - num = len(rl) - - if num == 0: - msg = _("No %(name)s matching %(args)s.") % { - 'name': self.resource_class.__name__, - 'args': kwargs - } - raise exceptions.NotFound(msg) - elif num > 1: - raise exceptions.NoUniqueMatch - else: - return rl[0] - - -class Extension(HookableMixin): - """Extension descriptor.""" - - SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') - manager_class = None - - def __init__(self, name, module): - super(Extension, self).__init__() - self.name = name - self.module = module - self._parse_extension_module() - - def _parse_extension_module(self): - self.manager_class = None - for attr_name, attr_value in self.module.__dict__.items(): - if attr_name in self.SUPPORTED_HOOKS: - self.add_hook(attr_name, attr_value) - else: - try: - if issubclass(attr_value, BaseManager): - self.manager_class = attr_value - except TypeError: - pass - - def __repr__(self): - return "" % self.name - - -class Resource(object): - """Base class for OpenStack resources (tenant, user, etc.). - - 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): - 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 six.iteritems(info): - try: - setattr(self, k, v) - self._info[k] = v - except AttributeError: - # In this case we already defined the attribute on the class - pass - - def __getattr__(self, k): - if k not in self.__dict__: - # NOTE(bcwaldon): disallow lazy-loading if already loaded once - if not self.is_loaded(): - self.get() - return self.__getattr__(k) - - raise AttributeError(k) - else: - return self.__dict__[k] - - 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'): - return - - new = self.manager.get(self.id) - if new: - self._add_details(new._info) - self._add_details( - {'x_request_id': self.manager.client.last_request_id}) - - def __eq__(self, other): - 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 is_loaded(self): - return self._loaded - - def set_loaded(self, val): - self._loaded = val - - def to_dict(self): - return copy.deepcopy(self._info) diff --git a/keystoneclient/openstack/common/apiclient/client.py b/keystoneclient/openstack/common/apiclient/client.py deleted file mode 100644 index bb6be1267..000000000 --- a/keystoneclient/openstack/common/apiclient/client.py +++ /dev/null @@ -1,388 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack Foundation -# Copyright 2011 Piston Cloud Computing, Inc. -# Copyright 2013 Alessio Ababilov -# Copyright 2013 Grid Dynamics -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -OpenStack Client interface. Handles the REST calls and responses. -""" - -# E0202: An attribute inherited from %s hide this method -# pylint: disable=E0202 - -import hashlib -import logging -import time - -try: - import simplejson as json -except ImportError: - import json - -from oslo_utils import encodeutils -from oslo_utils import importutils -import requests - -from keystoneclient.openstack.common._i18n import _ -from keystoneclient.openstack.common.apiclient import exceptions - -_logger = logging.getLogger(__name__) -SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',) - - -class HTTPClient(object): - """This client handles sending HTTP requests to OpenStack servers. - - Features: - - - share authentication information between several clients to different - services (e.g., for compute and image clients); - - reissue authentication request for expired tokens; - - encode/decode JSON bodies; - - raise exceptions on HTTP errors; - - pluggable authentication; - - store authentication information in a keyring; - - store time spent for requests; - - register clients for particular services, so one can use - `http_client.identity` or `http_client.compute`; - - log requests and responses in a format that is easy to copy-and-paste - into terminal and send the same request with curl. - """ - - user_agent = "keystoneclient.openstack.common.apiclient" - - def __init__(self, - auth_plugin, - region_name=None, - endpoint_type="publicURL", - original_ip=None, - verify=True, - cert=None, - timeout=None, - timings=False, - keyring_saver=None, - debug=False, - user_agent=None, - http=None): - self.auth_plugin = auth_plugin - - self.endpoint_type = endpoint_type - self.region_name = region_name - - self.original_ip = original_ip - self.timeout = timeout - self.verify = verify - self.cert = cert - - self.keyring_saver = keyring_saver - self.debug = debug - self.user_agent = user_agent or self.user_agent - - self.times = [] # [("item", starttime, endtime), ...] - self.timings = timings - - # requests within the same session can reuse TCP connections from pool - self.http = http or requests.Session() - - self.cached_token = None - self.last_request_id = None - - def _safe_header(self, name, value): - if name in SENSITIVE_HEADERS: - # because in python3 byte string handling is ... ug - v = value.encode('utf-8') - h = hashlib.sha1(v) - d = h.hexdigest() - return encodeutils.safe_decode(name), "{SHA1}%s" % d - else: - return (encodeutils.safe_decode(name), - encodeutils.safe_decode(value)) - - def _http_log_req(self, method, url, kwargs): - if not self.debug: - return - - string_parts = [ - "curl -g -i", - "-X '%s'" % method, - "'%s'" % url, - ] - - for element in kwargs['headers']: - header = ("-H '%s: %s'" % - self._safe_header(element, kwargs['headers'][element])) - string_parts.append(header) - - _logger.debug("REQ: %s" % " ".join(string_parts)) - if 'data' in kwargs: - _logger.debug("REQ BODY: %s\n" % (kwargs['data'])) - - def _http_log_resp(self, resp): - if not self.debug: - return - _logger.debug( - "RESP: [%s] %s\n", - resp.status_code, - resp.headers) - if resp._content_consumed: - _logger.debug( - "RESP BODY: %s\n", - resp.text) - - def serialize(self, kwargs): - if kwargs.get('json') is not None: - kwargs['headers']['Content-Type'] = 'application/json' - kwargs['data'] = json.dumps(kwargs['json']) - try: - del kwargs['json'] - except KeyError: - pass - - def get_timings(self): - return self.times - - def reset_timings(self): - self.times = [] - - def request(self, method, url, **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. - - :param method: method of HTTP request - :param url: URL of HTTP request - :param kwargs: any other parameter that can be passed to - requests.Session.request (such as `headers`) or `json` - that will be encoded as JSON and used as `data` argument - """ - kwargs.setdefault("headers", {}) - kwargs["headers"]["User-Agent"] = self.user_agent - if self.original_ip: - kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( - self.original_ip, self.user_agent) - if self.timeout is not None: - kwargs.setdefault("timeout", self.timeout) - kwargs.setdefault("verify", self.verify) - if self.cert is not None: - kwargs.setdefault("cert", self.cert) - self.serialize(kwargs) - - self._http_log_req(method, url, kwargs) - if self.timings: - start_time = time.time() - resp = self.http.request(method, url, **kwargs) - if self.timings: - self.times.append(("%s %s" % (method, url), - start_time, time.time())) - self._http_log_resp(resp) - - self.last_request_id = resp.headers.get('x-openstack-request-id') - - if resp.status_code >= 400: - _logger.debug( - "Request returned failure status: %s", - resp.status_code) - raise exceptions.from_response(resp, method, url) - - return resp - - @staticmethod - def concat_url(endpoint, url): - """Concatenate endpoint and final URL. - - E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to - "http://keystone/v2.0/tokens". - - :param endpoint: the base URL - :param url: the final URL - """ - return "%s/%s" % (endpoint.rstrip("/"), url.strip("/")) - - def client_request(self, client, method, url, **kwargs): - """Send an http request using `client`'s endpoint and specified `url`. - - If request was rejected as unauthorized (possibly because the token is - expired), issue one authorization attempt and send the request once - again. - - :param client: instance of BaseClient descendant - :param method: method of HTTP request - :param url: URL of HTTP request - :param kwargs: any other parameter that can be passed to - `HTTPClient.request` - """ - - filter_args = { - "endpoint_type": client.endpoint_type or self.endpoint_type, - "service_type": client.service_type, - } - token, endpoint = (self.cached_token, client.cached_endpoint) - just_authenticated = False - if not (token and endpoint): - try: - token, endpoint = self.auth_plugin.token_and_endpoint( - **filter_args) - except exceptions.EndpointException: - pass - if not (token and endpoint): - self.authenticate() - just_authenticated = True - token, endpoint = self.auth_plugin.token_and_endpoint( - **filter_args) - if not (token and endpoint): - raise exceptions.AuthorizationFailure( - _("Cannot find endpoint or token for request")) - - old_token_endpoint = (token, endpoint) - kwargs.setdefault("headers", {})["X-Auth-Token"] = token - self.cached_token = token - client.cached_endpoint = endpoint - # Perform the request once. If we get Unauthorized, then it - # might be because the auth token expired, so try to - # re-authenticate and try again. If it still fails, bail. - try: - return self.request( - method, self.concat_url(endpoint, url), **kwargs) - except exceptions.Unauthorized as unauth_ex: - if just_authenticated: - raise - self.cached_token = None - client.cached_endpoint = None - if self.auth_plugin.opts.get('token'): - self.auth_plugin.opts['token'] = None - if self.auth_plugin.opts.get('endpoint'): - self.auth_plugin.opts['endpoint'] = None - self.authenticate() - try: - token, endpoint = self.auth_plugin.token_and_endpoint( - **filter_args) - except exceptions.EndpointException: - raise unauth_ex - if (not (token and endpoint) or - old_token_endpoint == (token, endpoint)): - raise unauth_ex - self.cached_token = token - client.cached_endpoint = endpoint - kwargs["headers"]["X-Auth-Token"] = token - return self.request( - method, self.concat_url(endpoint, url), **kwargs) - - def add_client(self, base_client_instance): - """Add a new instance of :class:`BaseClient` descendant. - - `self` will store a reference to `base_client_instance`. - - Example: - - >>> def test_clients(): - ... from keystoneclient.auth import keystone - ... from openstack.common.apiclient import client - ... auth = keystone.KeystoneAuthPlugin( - ... username="user", password="pass", tenant_name="tenant", - ... auth_url="http://auth:5000/v2.0") - ... openstack_client = client.HTTPClient(auth) - ... # create nova client - ... from novaclient.v1_1 import client - ... client.Client(openstack_client) - ... # create keystone client - ... from keystoneclient.v2_0 import client - ... client.Client(openstack_client) - ... # use them - ... openstack_client.identity.tenants.list() - ... openstack_client.compute.servers.list() - """ - service_type = base_client_instance.service_type - if service_type and not hasattr(self, service_type): - setattr(self, service_type, base_client_instance) - - def authenticate(self): - self.auth_plugin.authenticate(self) - # Store the authentication results in the keyring for later requests - if self.keyring_saver: - self.keyring_saver.save(self) - - -class BaseClient(object): - """Top-level object to access the OpenStack API. - - This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient` - will handle a bunch of issues such as authentication. - """ - - service_type = None - endpoint_type = None # "publicURL" will be used - cached_endpoint = None - - def __init__(self, http_client, extensions=None): - self.http_client = http_client - http_client.add_client(self) - - # Add in any extensions... - if extensions: - for extension in extensions: - if extension.manager_class: - setattr(self, extension.name, - extension.manager_class(self)) - - def client_request(self, method, url, **kwargs): - return self.http_client.client_request( - self, method, url, **kwargs) - - @property - def last_request_id(self): - return self.http_client.last_request_id - - def head(self, url, **kwargs): - return self.client_request("HEAD", url, **kwargs) - - def get(self, url, **kwargs): - return self.client_request("GET", url, **kwargs) - - def post(self, url, **kwargs): - return self.client_request("POST", url, **kwargs) - - def put(self, url, **kwargs): - return self.client_request("PUT", url, **kwargs) - - def delete(self, url, **kwargs): - return self.client_request("DELETE", url, **kwargs) - - def patch(self, url, **kwargs): - return self.client_request("PATCH", url, **kwargs) - - @staticmethod - def get_class(api_name, version, version_map): - """Returns the client class for the requested API version - - :param api_name: the name of the API, e.g. 'compute', 'image', etc - :param version: the requested API version - :param version_map: a dict of client classes keyed by version - :rtype: a client class for the requested API version - """ - try: - client_path = version_map[str(version)] - except (KeyError, ValueError): - msg = _("Invalid %(api_name)s client version '%(version)s'. " - "Must be one of: %(version_map)s") % { - 'api_name': api_name, - 'version': version, - 'version_map': ', '.join(version_map.keys())} - raise exceptions.UnsupportedVersion(msg) - - return importutils.import_class(client_path) diff --git a/keystoneclient/openstack/common/apiclient/exceptions.py b/keystoneclient/openstack/common/apiclient/exceptions.py deleted file mode 100644 index a44492d75..000000000 --- a/keystoneclient/openstack/common/apiclient/exceptions.py +++ /dev/null @@ -1,479 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 Nebula, Inc. -# Copyright 2013 Alessio Ababilov -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Exception definitions. -""" - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-keystoneclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - -import inspect -import sys - -import six - -from keystoneclient.openstack.common._i18n import _ - - -class ClientException(Exception): - """The base exception class for all exceptions this library raises. - """ - pass - - -class ValidationError(ClientException): - """Error in validation on API client side.""" - pass - - -class UnsupportedVersion(ClientException): - """User is trying to use an unsupported version of the API.""" - pass - - -class CommandError(ClientException): - """Error in CLI tool.""" - pass - - -class AuthorizationFailure(ClientException): - """Cannot authorize API client.""" - pass - - -class ConnectionError(ClientException): - """Cannot connect to API service.""" - pass - - -class ConnectionRefused(ConnectionError): - """Connection refused while trying to connect to API service.""" - pass - - -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 EndpointException(ClientException): - """Something is rotten in Service Catalog.""" - pass - - -class EndpointNotFound(EndpointException): - """Could not find requested endpoint in Service Catalog.""" - pass - - -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 HttpError(ClientException): - """The base exception class for all HTTP exceptions. - """ - http_status = 0 - message = _("HTTP Error") - - def __init__(self, message=None, details=None, - response=None, request_id=None, - url=None, method=None, http_status=None): - self.http_status = http_status or self.http_status - self.message = message or self.message - self.details = details - self.request_id = request_id - self.response = response - self.url = url - self.method = method - formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) - if request_id: - formatted_string += " (Request-ID: %s)" % request_id - super(HttpError, self).__init__(formatted_string) - - -class HTTPRedirection(HttpError): - """HTTP Redirection.""" - message = _("HTTP Redirection") - - -class HTTPClientError(HttpError): - """Client-side HTTP error. - - Exception for cases in which the client seems to have erred. - """ - message = _("HTTP Client Error") - - -class HttpServerError(HttpError): - """Server-side HTTP error. - - Exception for cases in which the server is aware that it has - erred or is incapable of performing the request. - """ - message = _("HTTP Server Error") - - -class MultipleChoices(HTTPRedirection): - """HTTP 300 - Multiple Choices. - - Indicates multiple options for the resource that the client may follow. - """ - - http_status = 300 - message = _("Multiple Choices") - - -class BadRequest(HTTPClientError): - """HTTP 400 - Bad Request. - - The request cannot be fulfilled due to bad syntax. - """ - http_status = 400 - message = _("Bad Request") - - -class Unauthorized(HTTPClientError): - """HTTP 401 - Unauthorized. - - Similar to 403 Forbidden, but specifically for use when authentication - is required and has failed or has not yet been provided. - """ - http_status = 401 - message = _("Unauthorized") - - -class PaymentRequired(HTTPClientError): - """HTTP 402 - Payment Required. - - Reserved for future use. - """ - http_status = 402 - message = _("Payment Required") - - -class Forbidden(HTTPClientError): - """HTTP 403 - Forbidden. - - The request was a valid request, but the server is refusing to respond - to it. - """ - http_status = 403 - message = _("Forbidden") - - -class NotFound(HTTPClientError): - """HTTP 404 - Not Found. - - The requested resource could not be found but may be available again - in the future. - """ - http_status = 404 - message = _("Not Found") - - -class MethodNotAllowed(HTTPClientError): - """HTTP 405 - Method Not Allowed. - - A request was made of a resource using a request method not supported - by that resource. - """ - http_status = 405 - message = _("Method Not Allowed") - - -class NotAcceptable(HTTPClientError): - """HTTP 406 - Not Acceptable. - - The requested resource is only capable of generating content not - acceptable according to the Accept headers sent in the request. - """ - http_status = 406 - message = _("Not Acceptable") - - -class ProxyAuthenticationRequired(HTTPClientError): - """HTTP 407 - Proxy Authentication Required. - - The client must first authenticate itself with the proxy. - """ - http_status = 407 - message = _("Proxy Authentication Required") - - -class RequestTimeout(HTTPClientError): - """HTTP 408 - Request Timeout. - - The server timed out waiting for the request. - """ - http_status = 408 - message = _("Request Timeout") - - -class Conflict(HTTPClientError): - """HTTP 409 - Conflict. - - Indicates that the request could not be processed because of conflict - in the request, such as an edit conflict. - """ - http_status = 409 - message = _("Conflict") - - -class Gone(HTTPClientError): - """HTTP 410 - Gone. - - Indicates that the resource requested is no longer available and will - not be available again. - """ - http_status = 410 - message = _("Gone") - - -class LengthRequired(HTTPClientError): - """HTTP 411 - Length Required. - - The request did not specify the length of its content, which is - required by the requested resource. - """ - http_status = 411 - message = _("Length Required") - - -class PreconditionFailed(HTTPClientError): - """HTTP 412 - Precondition Failed. - - The server does not meet one of the preconditions that the requester - put on the request. - """ - http_status = 412 - message = _("Precondition Failed") - - -class RequestEntityTooLarge(HTTPClientError): - """HTTP 413 - Request Entity Too Large. - - The request is larger than the server is willing or able to process. - """ - http_status = 413 - message = _("Request Entity Too Large") - - def __init__(self, *args, **kwargs): - try: - self.retry_after = int(kwargs.pop('retry_after')) - except (KeyError, ValueError): - self.retry_after = 0 - - super(RequestEntityTooLarge, self).__init__(*args, **kwargs) - - -class RequestUriTooLong(HTTPClientError): - """HTTP 414 - Request-URI Too Long. - - The URI provided was too long for the server to process. - """ - http_status = 414 - message = _("Request-URI Too Long") - - -class UnsupportedMediaType(HTTPClientError): - """HTTP 415 - Unsupported Media Type. - - The request entity has a media type which the server or resource does - not support. - """ - http_status = 415 - message = _("Unsupported Media Type") - - -class RequestedRangeNotSatisfiable(HTTPClientError): - """HTTP 416 - Requested Range Not Satisfiable. - - The client has asked for a portion of the file, but the server cannot - supply that portion. - """ - http_status = 416 - message = _("Requested Range Not Satisfiable") - - -class ExpectationFailed(HTTPClientError): - """HTTP 417 - Expectation Failed. - - The server cannot meet the requirements of the Expect request-header field. - """ - http_status = 417 - message = _("Expectation Failed") - - -class UnprocessableEntity(HTTPClientError): - """HTTP 422 - Unprocessable Entity. - - The request was well-formed but was unable to be followed due to semantic - errors. - """ - http_status = 422 - message = _("Unprocessable Entity") - - -class InternalServerError(HttpServerError): - """HTTP 500 - Internal Server Error. - - A generic error message, given when no more specific message is suitable. - """ - http_status = 500 - message = _("Internal Server Error") - - -# NotImplemented is a python keyword. -class HttpNotImplemented(HttpServerError): - """HTTP 501 - Not Implemented. - - The server either does not recognize the request method, or it lacks - the ability to fulfill the request. - """ - http_status = 501 - message = _("Not Implemented") - - -class BadGateway(HttpServerError): - """HTTP 502 - Bad Gateway. - - The server was acting as a gateway or proxy and received an invalid - response from the upstream server. - """ - http_status = 502 - message = _("Bad Gateway") - - -class ServiceUnavailable(HttpServerError): - """HTTP 503 - Service Unavailable. - - The server is currently unavailable. - """ - http_status = 503 - message = _("Service Unavailable") - - -class GatewayTimeout(HttpServerError): - """HTTP 504 - Gateway Timeout. - - The server was acting as a gateway or proxy and did not receive a timely - response from the upstream server. - """ - http_status = 504 - message = _("Gateway Timeout") - - -class HttpVersionNotSupported(HttpServerError): - """HTTP 505 - HttpVersion Not Supported. - - The server does not support the HTTP protocol version used in the request. - """ - http_status = 505 - message = _("HTTP Version Not Supported") - - -# _code_map contains all the classes that have http_status attribute. -_code_map = dict( - (getattr(obj, 'http_status', None), obj) - for name, obj in six.iteritems(vars(sys.modules[__name__])) - if inspect.isclass(obj) and getattr(obj, 'http_status', False) -) - - -def from_response(response, method, url): - """Returns an instance of :class:`HttpError` or subclass based on response. - - :param response: instance of `requests.Response` class - :param method: HTTP method used for request - :param url: URL used for request - """ - - req_id = response.headers.get("x-openstack-request-id") - # NOTE(hdd) true for older versions of nova and cinder - if not req_id: - req_id = response.headers.get("x-compute-request-id") - kwargs = { - "http_status": response.status_code, - "response": response, - "method": method, - "url": url, - "request_id": req_id, - } - if "retry-after" in response.headers: - kwargs["retry_after"] = response.headers["retry-after"] - - content_type = response.headers.get("Content-Type", "") - if content_type.startswith("application/json"): - try: - body = response.json() - except ValueError: - pass - else: - if isinstance(body, dict): - error = body.get(list(body)[0]) - if isinstance(error, dict): - kwargs["message"] = (error.get("message") or - error.get("faultstring")) - kwargs["details"] = (error.get("details") or - six.text_type(body)) - elif content_type.startswith("text/"): - kwargs["details"] = getattr(response, 'text', '') - - try: - cls = _code_map[response.status_code] - except KeyError: - if 500 <= response.status_code < 600: - cls = HttpServerError - elif 400 <= response.status_code < 500: - cls = HTTPClientError - else: - cls = HttpError - return cls(**kwargs) diff --git a/keystoneclient/openstack/common/apiclient/fake_client.py b/keystoneclient/openstack/common/apiclient/fake_client.py deleted file mode 100644 index 0bb61851a..000000000 --- a/keystoneclient/openstack/common/apiclient/fake_client.py +++ /dev/null @@ -1,190 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -A fake server that "responds" to API methods with pre-canned responses. - -All of these responses come from the spec, so if for some reason the spec's -wrong the tests might raise AssertionError. I've indicated in comments the -places where actual behavior differs from the spec. -""" - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-keystoneclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - -# W0102: Dangerous default value %s as argument -# pylint: disable=W0102 - -import json - -import requests -import six -from six.moves.urllib import parse - -from keystoneclient.openstack.common.apiclient import client - - -def assert_has_keys(dct, required=None, optional=None): - required = required or [] - optional = optional or [] - for k in required: - try: - assert k in dct - except AssertionError: - extra_keys = set(dct.keys()).difference(set(required + optional)) - raise AssertionError("found unexpected keys: %s" % - list(extra_keys)) - - -class TestResponse(requests.Response): - """Wrap requests.Response and provide a convenient initialization. - """ - - def __init__(self, data): - super(TestResponse, self).__init__() - self._content_consumed = True - if isinstance(data, dict): - self.status_code = data.get('status_code', 200) - # Fake the text attribute to streamline Response creation - text = data.get('text', "") - if isinstance(text, (dict, list)): - self._content = json.dumps(text) - default_headers = { - "Content-Type": "application/json", - } - else: - self._content = text - default_headers = {} - if six.PY3 and isinstance(self._content, six.string_types): - self._content = self._content.encode('utf-8', 'strict') - self.headers = data.get('headers') or default_headers - else: - self.status_code = data - - def __eq__(self, other): - return (self.status_code == other.status_code and - self.headers == other.headers and - self._content == other._content) - - -class FakeHTTPClient(client.HTTPClient): - - def __init__(self, *args, **kwargs): - self.callstack = [] - self.fixtures = kwargs.pop("fixtures", None) or {} - if not args and "auth_plugin" not in kwargs: - args = (None, ) - super(FakeHTTPClient, self).__init__(*args, **kwargs) - - def assert_called(self, method, url, body=None, pos=-1): - """Assert than an API method was just called. - """ - expected = (method, url) - called = self.callstack[pos][0:2] - assert self.callstack, \ - "Expected %s %s but no calls were made." % expected - - assert expected == called, 'Expected %s %s; got %s %s' % \ - (expected + called) - - if body is not None: - if self.callstack[pos][3] != body: - raise AssertionError('%r != %r' % - (self.callstack[pos][3], body)) - - def assert_called_anytime(self, method, url, body=None): - """Assert than an API method was called anytime in the test. - """ - expected = (method, url) - - assert self.callstack, \ - "Expected %s %s but no calls were made." % expected - - found = False - entry = None - for entry in self.callstack: - if expected == entry[0:2]: - found = True - break - - assert found, 'Expected %s %s; got %s' % \ - (method, url, self.callstack) - if body is not None: - assert entry[3] == body, "%s != %s" % (entry[3], body) - - self.callstack = [] - - def clear_callstack(self): - self.callstack = [] - - def authenticate(self): - pass - - def client_request(self, client, method, url, **kwargs): - # Check that certain things are called correctly - if method in ["GET", "DELETE"]: - assert "json" not in kwargs - - # Note the call - self.callstack.append( - (method, - url, - kwargs.get("headers") or {}, - kwargs.get("json") or kwargs.get("data"))) - try: - fixture = self.fixtures[url][method] - except KeyError: - pass - else: - return TestResponse({"headers": fixture[0], - "text": fixture[1]}) - - # Call the method - args = parse.parse_qsl(parse.urlparse(url)[4]) - kwargs.update(args) - munged_url = url.rsplit('?', 1)[0] - munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') - munged_url = munged_url.replace('-', '_') - - callback = "%s_%s" % (method.lower(), munged_url) - - if not hasattr(self, callback): - raise AssertionError('Called unknown API method: %s %s, ' - 'expected fakes method name: %s' % - (method, url, callback)) - - resp = getattr(self, callback)(**kwargs) - if len(resp) == 3: - status, headers, body = resp - else: - status, body = resp - headers = {} - self.last_request_id = headers.get('x-openstack-request-id', - 'req-test') - return TestResponse({ - "status_code": status, - "text": body, - "headers": headers, - }) diff --git a/keystoneclient/openstack/common/apiclient/utils.py b/keystoneclient/openstack/common/apiclient/utils.py deleted file mode 100644 index 6f4709884..000000000 --- a/keystoneclient/openstack/common/apiclient/utils.py +++ /dev/null @@ -1,100 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-keystoneclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - -from oslo_utils import encodeutils -from oslo_utils import uuidutils -import six - -from keystoneclient.openstack.common._i18n import _ -from keystoneclient.openstack.common.apiclient import exceptions - - -def find_resource(manager, name_or_id, **find_args): - """Look for resource in a given manager. - - Used as a helper for the _find_* methods. - Example: - - .. code-block:: python - - def _find_hypervisor(cs, hypervisor): - #Get a hypervisor by name or ID. - return cliutils.find_resource(cs.hypervisors, hypervisor) - """ - # first try to get entity as integer id - try: - return manager.get(int(name_or_id)) - except (TypeError, ValueError, exceptions.NotFound): - pass - - # now try to get entity as uuid - try: - if six.PY2: - tmp_id = encodeutils.safe_encode(name_or_id) - else: - tmp_id = encodeutils.safe_decode(name_or_id) - - if uuidutils.is_uuid_like(tmp_id): - return manager.get(tmp_id) - except (TypeError, ValueError, exceptions.NotFound): - pass - - # for str id which is not uuid - if getattr(manager, 'is_alphanum_id_allowed', False): - try: - return manager.get(name_or_id) - except exceptions.NotFound: - pass - - try: - try: - return manager.find(human_id=name_or_id, **find_args) - except exceptions.NotFound: - pass - - # finally try to find entity by name - try: - resource = getattr(manager, 'resource_class', None) - name_attr = resource.NAME_ATTR if resource else 'name' - kwargs = {name_attr: name_or_id} - kwargs.update(find_args) - return manager.find(**kwargs) - except exceptions.NotFound: - msg = _("No %(name)s with a name or " - "ID of '%(name_or_id)s' exists.") % \ - { - "name": manager.resource_class.__name__.lower(), - "name_or_id": name_or_id - } - raise exceptions.CommandError(msg) - except exceptions.NoUniqueMatch: - msg = _("Multiple %(name)s matches found for " - "'%(name_or_id)s', use an ID to be more specific.") % \ - { - "name": manager.resource_class.__name__.lower(), - "name_or_id": name_or_id - } - raise exceptions.CommandError(msg) diff --git a/keystoneclient/openstack/common/memorycache.py b/keystoneclient/openstack/common/memorycache.py deleted file mode 100644 index e72c26df1..000000000 --- a/keystoneclient/openstack/common/memorycache.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Super simple fake memcache client.""" - -import copy - -from oslo_config import cfg -from oslo_utils import timeutils - -memcache_opts = [ - cfg.ListOpt('memcached_servers', - help='Memcached servers or None for in process cache.'), -] - -CONF = cfg.CONF -CONF.register_opts(memcache_opts) - - -def list_opts(): - """Entry point for oslo-config-generator.""" - return [(None, copy.deepcopy(memcache_opts))] - - -def get_client(memcached_servers=None): - client_cls = Client - - if not memcached_servers: - memcached_servers = CONF.memcached_servers - if memcached_servers: - import memcache - client_cls = memcache.Client - - return client_cls(memcached_servers, debug=0) - - -class Client(object): - """Replicates a tiny subset of memcached client interface.""" - - def __init__(self, *args, **kwargs): - """Ignores the passed in args.""" - self.cache = {} - - def get(self, key): - """Retrieves the value for a key or None. - - This expunges expired keys during each get. - """ - - now = timeutils.utcnow_ts() - for k in list(self.cache): - (timeout, _value) = self.cache[k] - if timeout and now >= timeout: - del self.cache[k] - - return self.cache.get(key, (0, None))[1] - - def set(self, key, value, time=0, min_compress_len=0): - """Sets the value for a key.""" - timeout = 0 - if time != 0: - timeout = timeutils.utcnow_ts() + time - self.cache[key] = (timeout, value) - return True - - def add(self, key, value, time=0, min_compress_len=0): - """Sets the value for a key if it doesn't exist.""" - if self.get(key) is not None: - return False - return self.set(key, value, time, min_compress_len) - - def incr(self, key, delta=1): - """Increments the value for a key.""" - value = self.get(key) - if value is None: - return None - new_value = int(value) + delta - self.cache[key] = (self.cache[key][0], str(new_value)) - return new_value - - def delete(self, key, time=0): - """Deletes the value associated with a key.""" - if key in self.cache: - del self.cache[key] diff --git a/keystoneclient/service_catalog.py b/keystoneclient/service_catalog.py index 143a6b7c6..afbefc0ae 100644 --- a/keystoneclient/service_catalog.py +++ b/keystoneclient/service_catalog.py @@ -17,21 +17,34 @@ # limitations under the License. import abc - -import six +import warnings from keystoneclient import exceptions from keystoneclient.i18n import _ -from keystoneclient import utils -@six.add_metaclass(abc.ABCMeta) -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. + + .. 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.""" + """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): @@ -40,13 +53,32 @@ def factory(cls, resource_dict, token=None, region_name=None): 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): - # FIXME(jamielennox): Having region_name set on the service catalog - # directly is deprecated. It should instead be provided as a parameter - # to calls made to the service_catalog. Provide appropriate warning. + """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): @@ -65,7 +97,7 @@ def get_token(self): - `domain_id`: Authorized domain's ID """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover @abc.abstractmethod def _is_endpoint_type_match(self, endpoint, endpoint_type): @@ -74,6 +106,7 @@ def _is_endpoint_type_match(self, endpoint, endpoint_type): :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): @@ -86,6 +119,7 @@ def _normalize_endpoint_type(self, 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): @@ -119,7 +153,7 @@ def get_endpoints(self, service_type=None, endpoint_type=None, if service_name: try: sn = service['name'] - except KeyError: + 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 @@ -142,8 +176,10 @@ def get_endpoints(self, service_type=None, endpoint_type=None, def _get_service_endpoints(self, attr, filter_value, service_type, endpoint_type, region_name, service_name): - """Fetch the endpoints of a particular service_type and handle - the filtering. + """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, @@ -155,10 +191,13 @@ def _get_service_endpoints(self, attr, filter_value, service_type, except KeyError: return - # TODO(jamielennox): at least swiftclient is known to set attr and not - # filter_value and expects that to mean that filtering is ignored, so - # we can't check for the presence of attr. This behaviour should be - # deprecated and an appropriate warning provided. + 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] @@ -166,7 +205,6 @@ def _get_service_endpoints(self, attr, filter_value, service_type, return endpoints @abc.abstractmethod - @utils.positional(enforcement=utils.positional.WARN) def get_urls(self, attr=None, filter_value=None, service_type='identity', endpoint_type='publicURL', region_name=None, service_name=None): @@ -188,9 +226,8 @@ def get_urls(self, attr=None, filter_value=None, :returns: tuple of urls or None (if no match found) """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover - @utils.positional(3, enforcement=utils.positional.WARN) def url_for(self, attr=None, filter_value=None, service_type='identity', endpoint_type='publicURL', region_name=None, service_name=None): @@ -225,33 +262,33 @@ def url_for(self, attr=None, filter_value=None, try: return urls[0] except Exception: - pass - - 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}) - - raise exceptions.EndpointNotFound(msg) + 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}) + + raise exceptions.EndpointNotFound(msg) @abc.abstractmethod def get_data(self): @@ -262,12 +299,13 @@ def get_data(self): :returns: list containing raw catalog data entries or None """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover class ServiceCatalogV2(ServiceCatalog): - """An object for encapsulating the service catalog using raw v2 auth token - from Keystone. + """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): @@ -283,7 +321,7 @@ def is_valid(cls, resource_dict): def _normalize_endpoint_type(self, endpoint_type): if endpoint_type and 'URL' not in endpoint_type: - endpoint_type = endpoint_type + 'URL' + endpoint_type += 'URL' return endpoint_type @@ -299,12 +337,11 @@ def get_token(self): try: token['user_id'] = self.catalog['user']['id'] token['tenant_id'] = self.catalog['token']['tenant']['id'] - except Exception: + except KeyError: # nosec(cjschaef) # just leave the tenant and user out if it doesn't exist pass return token - @utils.positional(enforcement=utils.positional.WARN) def get_urls(self, attr=None, filter_value=None, service_type='identity', endpoint_type='publicURL', region_name=None, service_name=None): @@ -323,8 +360,9 @@ def get_urls(self, attr=None, filter_value=None, class ServiceCatalogV3(ServiceCatalog): - """An object for encapsulating the service catalog using raw v3 auth token - from Keystone. + """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): @@ -365,12 +403,11 @@ def get_token(self): project = self.catalog.get('project') if project: token['tenant_id'] = project['id'] - except Exception: + except KeyError: # nosec(cjschaef) # just leave the domain, project and user out if it doesn't exist pass return token - @utils.positional(enforcement=utils.positional.WARN) def get_urls(self, attr=None, filter_value=None, service_type='identity', endpoint_type='public', region_name=None, service_name=None): diff --git a/keystoneclient/session.py b/keystoneclient/session.py index 73ec0226b..4944d45f6 100644 --- a/keystoneclient/session.py +++ b/keystoneclient/session.py @@ -17,22 +17,29 @@ 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 -import six -from six.moves import urllib from keystoneclient import exceptions -from keystoneclient.i18n import _, _LI, _LW -from keystoneclient import utils +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__) @@ -68,7 +75,7 @@ def _remove_service_catalog(body): data['access']['serviceCatalog'] = '' return jsonutils.dumps(data) - except Exception: + except Exception: # nosec(cjschaef): multiple exceptions can be raised # Don't fail trying to clean up the request body. pass return body @@ -116,21 +123,28 @@ class Session(object): _REDIRECT_STATUSES = (301, 302, 303, 305, 307) REDIRECT_STATUSES = _REDIRECT_STATUSES - """This property is deprecated.""" + """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.""" + """This property is deprecated as of the 1.7.0 release and may be removed + in the 2.0.0 release.""" - @utils.positional(2, enforcement=utils.positional.WARN) 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 session.adapters.keys(): + for scheme in list(session.adapters): session.mount(scheme, TCPKeepAliveAdapter()) self.auth = auth @@ -150,19 +164,20 @@ def __init__(self, auth=None, session=None, original_ip=None, verify=True, @staticmethod def _process_header(header): - """Redacts the secure headers to be logged.""" + """Redact the secure headers to be logged.""" secure_headers = ('authorization', 'x-auth-token', - 'x-subject-token',) + 'x-subject-token', 'x-service-token') if header[0].lower() in secure_headers: - token_hasher = hashlib.sha1() + # 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 - @utils.positional() def _http_log_request(self, url, method=None, data=None, - json=None, headers=None, logger=_logger): + 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 @@ -175,7 +190,7 @@ def _http_log_request(self, url, method=None, data=None, # so we need to actually check that this is False. if self.verify is False: string_parts.append('--insecure') - elif isinstance(self.verify, six.string_types): + elif isinstance(self.verify, str): string_parts.append('--cacert "%s"' % self.verify) if method: @@ -184,46 +199,61 @@ def _http_log_request(self, url, method=None, data=None, string_parts.append(url) if headers: - for header in six.iteritems(headers): + for header in headers.items(): string_parts.append('-H "%s: %s"' % self._process_header(header)) - if json: - data = jsonutils.dumps(json) + if data: + if isinstance(data, bytes): + try: + data = data.decode("ascii") + except UnicodeDecodeError: + data = "" string_parts.append("-d '%s'" % data) - - logger.debug(' '.join(string_parts)) - - @utils.positional() - def _http_log_response(self, response=None, json=None, - status_code=None, headers=None, text=None, - logger=_logger): + 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 - if response is not None: - if not status_code: - status_code = response.status_code - if not headers: - headers = response.headers - if not text: + # 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) - if json: - text = jsonutils.dumps(json) - - string_parts = ['RESP:'] - - if status_code: - string_parts.append('[%s]' % status_code) - if headers: - for header in six.iteritems(headers): - string_parts.append('%s: %s' % self._process_header(header)) - if text: - string_parts.append('\nRESP BODY: %s\n' % text) + 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)) - @utils.positional(enforcement=utils.positional.WARN) + # 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, @@ -303,7 +333,6 @@ def request(self, url, method, json=None, original_ip=None, :returns: The response to the request. """ - headers = kwargs.setdefault('headers', dict()) if authenticated is None: @@ -334,7 +363,10 @@ def request(self, url, method, json=None, original_ip=None, base_url = self.get_endpoint(auth, **endpoint_filter) if not base_url: - raise exceptions.EndpointNotFound() + 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('/')) @@ -379,6 +411,19 @@ def request(self, url, method, json=None, original_ip=None, 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 @@ -428,7 +473,7 @@ def _send_request(self, url, method, redirect, log, logger, if connect_retries <= 0: raise - logger.info(_LI('Failure: %(e)s. Retrying in %(delay).1fs.'), + logger.info('Failure: %(e)s. Retrying in %(delay).1fs.', {'e': e, 'delay': connect_retry_delay}) time.sleep(connect_retry_delay) @@ -439,7 +484,7 @@ def _send_request(self, url, method, redirect, log, logger, **kwargs) if log: - self._http_log_response(response=resp, logger=logger) + self._http_log_response(resp, logger) if resp.status_code in self._REDIRECT_STATUSES: # be careful here in python True == 1 and False == 0 @@ -455,8 +500,8 @@ def _send_request(self, url, method, redirect, log, logger, try: location = resp.headers['location'] except KeyError: - logger.warning(_LW("Failed to redirect request to %s as new " - "location was not provided."), resp.url) + 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. @@ -522,27 +567,40 @@ def patch(self, url, **kwargs): @classmethod def construct(cls, kwargs): - """Handles constructing a session from the older + """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*: 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. + + *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: + except KeyError: # nosec(cjschaef): we are brute force + # identifying possible attributes for kwargs pass return cls._make(**params) @@ -563,8 +621,11 @@ def _make(cls, insecure=False, verify=None, cacert=None, cert=None, verify = cacert or True if cert and key: - # passing cert and key together is deprecated in favour of the - # requests lib form of having the cert and key as a tuple + 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) @@ -597,6 +658,8 @@ def get_auth_headers(self, auth=None, **kwargs): 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. @@ -609,9 +672,12 @@ def get_token(self, auth=None): :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not available. - *DEPRECATED*: This assumes that the only header that is used to - authenticate a message is 'X-Auth-Token'. This may not be - correct. Use get_auth_headers instead. + .. 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 @@ -635,6 +701,60 @@ def get_endpoint(self, auth=None, **kwargs): 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. @@ -683,10 +803,9 @@ def get_project_id(self, auth=None): auth = self._auth_required(auth, msg) return auth.get_project_id(self) - @utils.positional.classmethod() + @classmethod def get_conf_options(cls, deprecated_opts=None): - """Get the oslo_config options that are needed for a - :py:class:`.Session`. + """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. @@ -733,7 +852,7 @@ def get_conf_options(cls, deprecated_opts=None): help='Timeout value for http requests'), ] - @utils.positional.classmethod() + @classmethod def register_conf_options(cls, conf, group, deprecated_opts=None): """Register the oslo_config options that are needed for a session. @@ -782,8 +901,8 @@ def load_from_conf_options(cls, conf, group, **kwargs): kwargs['insecure'] = c.insecure kwargs['cacert'] = c.cafile - kwargs['cert'] = c.certfile - kwargs['key'] = c.keyfile + if c.certfile and c.keyfile: + kwargs['cert'] = (c.certfile, c.keyfile) kwargs['timeout'] = c.timeout return cls._make(**kwargs) @@ -840,19 +959,59 @@ def load_from_cli_options(cls, args, **kwargs): """ kwargs['insecure'] = args.insecure kwargs['cacert'] = args.os_cacert - kwargs['cert'] = args.os_cert - kwargs['key'] = args.os_key + 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.""" + """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 requests.__version__ >= '2.4.1': - kwargs.setdefault('socket_options', [ + 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 4a6dbd26b..000000000 --- a/keystoneclient/shell.py +++ /dev/null @@ -1,472 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 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. - -"""Command-line interface to the OpenStack Identity API.""" - -from __future__ import print_function - -import argparse -import logging -import os -import sys -import warnings - -from oslo_utils import encodeutils -import six - -import keystoneclient -from keystoneclient import access -from keystoneclient.contrib.bootstrap import shell as shell_bootstrap -from keystoneclient import exceptions as exc -from keystoneclient.generic import shell as shell_generic -from keystoneclient import session -from keystoneclient import utils -from keystoneclient.v2_0 import shell as shell_v2_0 - - -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) - if value: - return value - return kwargs.get('default', '') - - -class OpenStackIdentityShell(object): - - def __init__(self, parser_class=argparse.ArgumentParser): - - # Since Python 2.7, DeprecationWarning is ignored by default, enable - # it so that the deprecation message is displayed. - warnings.simplefilter('once', category=DeprecationWarning) - warnings.warn( - 'The keystone CLI is deprecated in favor of ' - 'python-openstackclient. For a Python library, continue using ' - 'python-keystoneclient.', DeprecationWarning) - # And back to normal! - warnings.resetwarnings() - self.parser_class = parser_class - - def get_base_parser(self): - parser = self.parser_class( - 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__, - help="Shows the client version and exits.") - - parser.add_argument('--debug', - default=False, - action='store_true', - help="Prints debugging output onto the console, " - "this includes the curl request and response " - "calls. Helpful for debugging and " - "understanding the API calls.") - - 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='Specify the region to use. ' - '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='Specify Identity API version to use. ' - '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-cache', - default=env('OS_CACHE', default=False), - action='store_true', - help='Use the auth token cache. ' - 'Defaults 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 the keyring is available and in use, " - "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 the 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) - - session.Session.register_cli_options(parser) - - parser.add_argument('--os_cacert', help=argparse.SUPPRESS) - parser.add_argument('--os_key', help=argparse.SUPPRESS) - parser.add_argument('--os_cert', 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 hyphen-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 - group = subparser.add_argument_group(title='Arguments') - for (args, kwargs) in arguments: - group.add_argument(*args, **kwargs) - subparser.set_defaults(func=callback) - - def auth_check(self, args): - if args.os_token or 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]') - - # user supplied a token and endpoint and at least one other cred - if args.os_username or args.os_password or args.os_auth_url: - msg = ('WARNING: Bypassing authentication using a token & ' - 'endpoint (authentication credentials are being ' - 'ignored).') - print(msg) - - else: - if not args.os_auth_url: - raise exc.CommandError( - 'Expecting an auth URL via either --os-auth-url or ' - 'env[OS_AUTH_URL]') - - if args.os_username or args.os_password: - if not args.os_username: - raise exc.CommandError( - 'Expecting a username provided via either ' - '--os-username or env[OS_USERNAME]') - - if not args.os_password: - args.os_password = utils.prompt_user_password() - - # No password because we didn'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') - - else: - raise exc.CommandError('Expecting authentication method via' - '\n either a service token, ' - '--os-token or env[OS_SERVICE_TOKEN], ' - '\n credentials, ' - '--os-username or env[OS_USERNAME]') - - 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 - - if args.debug: - logging_level = logging.DEBUG - iso_logger = logging.getLogger('iso8601') - iso_logger.setLevel('WARN') - else: - logging_level = logging.WARNING - - logging.basicConfig(level=logging_level) - - # 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 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, - timeout=args.timeout) - else: - self.auth_check(args) - 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, - timeout=args.timeout) - - 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: - if version: - msg = ('WARNING: unsupported identity-api-version %s, ' - 'falling back to 2.0' % version) - print(msg) - 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 list(sc._optionals._option_string_actions): - 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): - INDENT_BEFORE_ARGUMENTS = 6 - MAX_WIDTH_ARGUMENTS = 32 - - def add_arguments(self, actions): - for action in filter(lambda x: not x.option_strings, actions): - if not action.choices: - continue - for choice in action.choices: - length = len(choice) + self.INDENT_BEFORE_ARGUMENTS - if(length > self._max_help_position and - length <= self.MAX_WIDTH_ARGUMENTS): - self._max_help_position = length - super(OpenStackHelpFormatter, self).add_arguments(actions) - - 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 KeyboardInterrupt: - print("... terminating keystone client", file=sys.stderr) - sys.exit(130) - except Exception as e: - print(encodeutils.safe_encode(six.text_type(e)), file=sys.stderr) - sys.exit(1) - - -if __name__ == "__main__": - sys.exit(main()) 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/hooks/post_test_hook.sh b/keystoneclient/tests/functional/hooks/post_test_hook.sh deleted file mode 100755 index 0a07aa77d..000000000 --- a/keystoneclient/tests/functional/hooks/post_test_hook.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -xe - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# 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 is executed inside post_test_hook function in devstack gate. - -function generate_testr_results { - if [ -f .testrepository/0 ]; then - sudo .tox/functional/bin/testr last --subunit > $WORKSPACE/testrepository.subunit - sudo mv $WORKSPACE/testrepository.subunit $BASE/logs/testrepository.subunit - sudo .tox/functional/bin/python /usr/local/jenkins/slave_scripts/subunit2html.py $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html - sudo gzip -9 $BASE/logs/testrepository.subunit - sudo gzip -9 $BASE/logs/testr_results.html - sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz - sudo chmod a+r $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz - fi -} - -export KEYSTONECLIENT_DIR="$BASE/new/python-keystoneclient" - -# Get admin credentials -cd $BASE/new/devstack -source openrc admin admin - -# Go to the keystoneclient dir -cd $KEYSTONECLIENT_DIR - -sudo chown -R jenkins:stack $KEYSTONECLIENT_DIR - -# Run tests -echo "Running keystoneclient functional test suite" -set +e -# Preserve env for OS_ credentials -sudo -E -H -u jenkins tox -efunctional -EXIT_CODE=$? -set -e - -# Collect and parse result -generate_testr_results -exit $EXIT_CODE diff --git a/keystoneclient/tests/functional/test_access.py b/keystoneclient/tests/functional/test_access.py deleted file mode 100644 index 36fe60ef6..000000000 --- a/keystoneclient/tests/functional/test_access.py +++ /dev/null @@ -1,47 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os - -from keystoneclient.auth.identity import v2 -from keystoneclient import session -from tempest_lib import base - - -class TestV2AccessInfo(base.BaseTestCase): - - def setUp(self): - super(TestV2AccessInfo, self).setUp() - - self.session = session.Session() - - def test_access_audit_id(self): - unscoped_plugin = v2.Password(auth_url=os.environ.get('OS_AUTH_URL'), - username=os.environ.get('OS_USERNAME'), - password=os.environ.get('OS_PASSWORD')) - - unscoped_auth_ref = unscoped_plugin.get_access(self.session) - - self.assertIsNotNone(unscoped_auth_ref.audit_id) - self.assertIsNone(unscoped_auth_ref.audit_chain_id) - - scoped_plugin = v2.Token(auth_url=os.environ.get('OS_AUTH_URL'), - token=unscoped_auth_ref.auth_token, - tenant_name=os.environ.get('OS_TENANT_NAME')) - - scoped_auth_ref = scoped_plugin.get_access(self.session) - - self.assertIsNotNone(scoped_auth_ref.audit_id) - self.assertIsNotNone(scoped_auth_ref.audit_chain_id) - - self.assertEqual(unscoped_auth_ref.audit_id, - scoped_auth_ref.audit_chain_id) 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/keystoneclient/tests/functional/test_cli.py b/keystoneclient/tests/functional/test_cli.py deleted file mode 100644 index 8400e7ce0..000000000 --- a/keystoneclient/tests/functional/test_cli.py +++ /dev/null @@ -1,143 +0,0 @@ - -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os -import re - -from tempest_lib.cli import base -from tempest_lib import exceptions - - -class SimpleReadOnlyKeystoneClientTest(base.ClientTestBase): - """Basic, read-only tests for Keystone CLI client. - - Checks return values and output of read-only commands. - These tests do not presume any content, nor do they create - their own. They only verify the structure of output if present. - """ - - def _get_clients(self): - path = os.path.join(os.path.abspath('.'), '.tox/functional/bin') - cli_dir = os.environ.get('OS_KEYSTONECLIENT_EXEC_DIR', path) - - return base.CLIClient( - username=os.environ.get('OS_USERNAME'), - password=os.environ.get('OS_PASSWORD'), - tenant_name=os.environ.get('OS_TENANT_NAME'), - uri=os.environ.get('OS_AUTH_URL'), - cli_dir=cli_dir) - - def keystone(self, *args, **kwargs): - return self.clients.keystone(*args, **kwargs) - - def test_admin_fake_action(self): - self.assertRaises(exceptions.CommandFailed, - self.keystone, - 'this-does-not-exist') - - def test_admin_catalog_list(self): - out = self.keystone('catalog') - catalog = self.parser.details_multiple(out, with_label=True) - for svc in catalog: - if svc.get('__label'): - self.assertTrue(svc['__label'].startswith('Service:'), - msg=('Invalid beginning of service block: ' - '%s' % svc['__label'])) - # check that region and publicURL exists. One might also - # check for adminURL and internalURL. id seems to be optional - # and is missing in the catalog backend - self.assertIn('publicURL', svc.keys()) - self.assertIn('region', svc.keys()) - - def test_admin_endpoint_list(self): - out = self.keystone('endpoint-list') - endpoints = self.parser.listing(out) - self.assertTableStruct(endpoints, [ - 'id', 'region', 'publicurl', 'internalurl', - 'adminurl', 'service_id']) - - def test_admin_endpoint_service_match(self): - endpoints = self.parser.listing(self.keystone('endpoint-list')) - services = self.parser.listing(self.keystone('service-list')) - svc_by_id = {} - for svc in services: - svc_by_id[svc['id']] = svc - for endpoint in endpoints: - self.assertIn(endpoint['service_id'], svc_by_id) - - def test_admin_role_list(self): - roles = self.parser.listing(self.keystone('role-list')) - self.assertTableStruct(roles, ['id', 'name']) - - def test_admin_service_list(self): - services = self.parser.listing(self.keystone('service-list')) - self.assertTableStruct(services, ['id', 'name', 'type', 'description']) - - def test_admin_tenant_list(self): - tenants = self.parser.listing(self.keystone('tenant-list')) - self.assertTableStruct(tenants, ['id', 'name', 'enabled']) - - def test_admin_user_list(self): - users = self.parser.listing(self.keystone('user-list')) - self.assertTableStruct(users, [ - 'id', 'name', 'enabled', 'email']) - - def test_admin_user_role_list(self): - user_roles = self.parser.listing(self.keystone('user-role-list')) - self.assertTableStruct(user_roles, [ - 'id', 'name', 'user_id', 'tenant_id']) - - def test_admin_discover(self): - discovered = self.keystone('discover') - self.assertIn('Keystone found at http', discovered) - self.assertIn('supports version', discovered) - - def test_admin_help(self): - help_text = self.keystone('help') - lines = help_text.split('\n') - self.assertFirstLineStartsWith(lines, 'usage: keystone') - - commands = [] - cmds_start = lines.index('Positional arguments:') - cmds_end = lines.index('Optional arguments:') - command_pattern = re.compile('^ {4}([a-z0-9\-\_]+)') - for line in lines[cmds_start:cmds_end]: - match = command_pattern.match(line) - if match: - commands.append(match.group(1)) - commands = set(commands) - wanted_commands = set(('catalog', 'endpoint-list', 'help', - 'token-get', 'discover', 'bootstrap')) - self.assertFalse(wanted_commands - commands) - - def test_admin_bashcompletion(self): - self.keystone('bash-completion') - - def test_admin_ec2_credentials_list(self): - creds = self.keystone('ec2-credentials-list') - creds = self.parser.listing(creds) - self.assertTableStruct(creds, ['tenant', 'access', 'secret']) - - # Optional arguments: - - def test_admin_version(self): - self.keystone('', flags='--version') - - def test_admin_debug_list(self): - self.keystone('catalog', flags='--debug') - - def test_admin_timeout(self): - self.keystone('catalog', flags='--timeout %d' % 15) diff --git a/keystoneclient/contrib/bootstrap/__init__.py b/keystoneclient/tests/functional/v3/__init__.py similarity index 100% rename from keystoneclient/contrib/bootstrap/__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/keystoneclient/tests/unit/apiclient/test_exceptions.py b/keystoneclient/tests/unit/apiclient/test_exceptions.py index 4a803c7e7..65cf08016 100644 --- a/keystoneclient/tests/unit/apiclient/test_exceptions.py +++ b/keystoneclient/tests/unit/apiclient/test_exceptions.py @@ -13,8 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six - from keystoneclient import exceptions from keystoneclient.tests.unit import utils @@ -23,7 +21,7 @@ class FakeResponse(object): json_data = {} def __init__(self, **kwargs): - for key, value in six.iteritems(kwargs): + for key, value in kwargs.items(): setattr(self, key, value) def json(self): @@ -40,7 +38,7 @@ def assert_exception(self, ex_cls, method, url, status_code, json_data): method, url) self.assertIsInstance(ex, ex_cls) - self.assertEqual(ex.message, json_data["error"]["message"]) + 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) diff --git a/keystoneclient/tests/unit/auth/test_access.py b/keystoneclient/tests/unit/auth/test_access.py index 405fb8bef..f5f5843a8 100644 --- a/keystoneclient/tests/unit/auth/test_access.py +++ b/keystoneclient/tests/unit/auth/test_access.py @@ -12,10 +12,11 @@ import uuid +from keystoneauth1 import fixture +from keystoneauth1 import plugin + from keystoneclient import access -from keystoneclient import auth from keystoneclient.auth.identity import access as access_plugin -from keystoneclient import fixture from keystoneclient import session from keystoneclient.tests.unit import utils @@ -24,7 +25,8 @@ class AccessInfoPluginTests(utils.TestCase): def setUp(self): super(AccessInfoPluginTests, self).setUp() - self.session = session.Session() + with self.deprecations.expect_deprecations_here(): + self.session = session.Session() self.auth_token = uuid.uuid4().hex def _plugin(self, **kwargs): @@ -34,7 +36,8 @@ def _plugin(self, **kwargs): auth_ref = access.AccessInfo.factory(body=token, auth_token=self.auth_token) - return access_plugin.AccessInfoPlugin(auth_ref, **kwargs) + with self.deprecations.expect_deprecations_here(): + return access_plugin.AccessInfoPlugin(auth_ref, **kwargs) def test_auth_ref(self): plugin = self._plugin() @@ -46,11 +49,11 @@ def test_auth_ref(self): def test_auth_url(self): auth_url = 'http://keystone.test.url' - plugin = self._plugin(auth_url=auth_url) + plug = self._plugin(auth_url=auth_url) self.assertEqual(auth_url, - plugin.get_endpoint(self.session, - interface=auth.AUTH_INTERFACE)) + plug.get_endpoint(self.session, + interface=plugin.AUTH_INTERFACE)) def test_invalidate(self): plugin = self._plugin() diff --git a/keystoneclient/tests/unit/auth/test_auth.py b/keystoneclient/tests/unit/auth/test_auth.py index f490f2dac..3a56fb2aa 100644 --- a/keystoneclient/tests/unit/auth/test_auth.py +++ b/keystoneclient/tests/unit/auth/test_auth.py @@ -10,27 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. -from keystoneclient import auth -from keystoneclient.auth import identity from keystoneclient.tests.unit.auth import utils class AuthTests(utils.TestCase): def test_plugin_names_in_available(self): - plugins = auth.get_available_plugin_names() - - for p in ('password', 'v2password', 'v3password', - 'token', 'v2token', 'v3token'): - self.assertIn(p, plugins) + pass def test_plugin_classes_in_available(self): - plugins = auth.get_available_plugin_classes() - - self.assertIs(plugins['password'], identity.Password) - self.assertIs(plugins['v2password'], identity.V2Password) - self.assertIs(plugins['v3password'], identity.V3Password) - - self.assertIs(plugins['token'], identity.Token) - self.assertIs(plugins['v2token'], identity.V2Token) - self.assertIs(plugins['v3token'], identity.V3Token) + pass diff --git a/keystoneclient/tests/unit/auth/test_cli.py b/keystoneclient/tests/unit/auth/test_cli.py index d65de731f..b2a2f6ae8 100644 --- a/keystoneclient/tests/unit/auth/test_cli.py +++ b/keystoneclient/tests/unit/auth/test_cli.py @@ -11,10 +11,10 @@ # under the License. import argparse +from unittest import mock import uuid import fixtures -import mock from oslo_config import cfg from keystoneclient.auth import base @@ -42,6 +42,7 @@ 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): diff --git a/keystoneclient/tests/unit/auth/test_conf.py b/keystoneclient/tests/unit/auth/test_conf.py index c3ce8eb69..bea37a4f1 100644 --- a/keystoneclient/tests/unit/auth/test_conf.py +++ b/keystoneclient/tests/unit/auth/test_conf.py @@ -10,17 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock import uuid -import mock from oslo_config import cfg from oslo_config import fixture as config -import stevedore from keystoneclient.auth import base from keystoneclient.auth import conf -from keystoneclient.auth.identity import v2 as v2_auth -from keystoneclient.auth.identity import v3 as v3_auth from keystoneclient import exceptions from keystoneclient.tests.unit.auth import utils @@ -29,6 +26,7 @@ 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 @@ -38,58 +36,10 @@ def setUp(self): conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) def test_loading_v2(self): - section = uuid.uuid4().hex - username = uuid.uuid4().hex - password = uuid.uuid4().hex - trust_id = uuid.uuid4().hex - tenant_id = uuid.uuid4().hex - - self.conf_fixture.config(auth_section=section, group=self.GROUP) - conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) - - self.conf_fixture.register_opts(v2_auth.Password.get_options(), - group=section) - - self.conf_fixture.config(auth_plugin=self.V2PASS, - username=username, - password=password, - trust_id=trust_id, - tenant_id=tenant_id, - group=section) - - a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) - - self.assertEqual(username, a.username) - self.assertEqual(password, a.password) - self.assertEqual(trust_id, a.trust_id) - self.assertEqual(tenant_id, a.tenant_id) + pass def test_loading_v3(self): - section = uuid.uuid4().hex - token = uuid.uuid4().hex - trust_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - project_domain_name = uuid.uuid4().hex - - self.conf_fixture.config(auth_section=section, group=self.GROUP) - conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) - - self.conf_fixture.register_opts(v3_auth.Token.get_options(), - group=section) - - self.conf_fixture.config(auth_plugin=self.V3TOKEN, - token=token, - trust_id=trust_id, - project_id=project_id, - project_domain_name=project_domain_name, - group=section) - - a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) - - self.assertEqual(token, a.auth_methods[0].token) - self.assertEqual(trust_id, a.trust_id) - self.assertEqual(project_id, a.project_id) - self.assertEqual(project_domain_name, a.project_domain_name) + pass def test_loading_invalid_plugin(self): auth_plugin = uuid.uuid4().hex @@ -154,15 +104,7 @@ def test_diff_section(self, m): self.assertTestVals(a) def test_plugins_are_all_opts(self): - manager = stevedore.ExtensionManager(base.PLUGIN_NAMESPACE, - invoke_on_load=False, - propagate_map_exceptions=True) - - def inner(driver): - for p in driver.plugin.get_options(): - self.assertIsInstance(p, cfg.Opt) - - manager.map(inner) + pass def test_get_common(self): opts = conf.get_common_conf_options() @@ -171,7 +113,4 @@ def test_get_common(self): self.assertEqual(2, len(opts)) def test_get_named(self): - loaded_opts = conf.get_plugin_options('v2password') - plugin_opts = v2_auth.Password.get_options() - - self.assertEqual(plugin_opts, loaded_opts) + pass diff --git a/keystoneclient/tests/unit/auth/test_default_cli.py b/keystoneclient/tests/unit/auth/test_default_cli.py index 2a9bad1f9..8afd2cde7 100644 --- a/keystoneclient/tests/unit/auth/test_default_cli.py +++ b/keystoneclient/tests/unit/auth/test_default_cli.py @@ -11,9 +11,9 @@ # under the License. import argparse +from unittest import mock import uuid -import mock from keystoneclient.auth.identity.generic import cli from keystoneclient import exceptions @@ -22,6 +22,10 @@ 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) diff --git a/keystoneclient/tests/unit/auth/test_identity_common.py b/keystoneclient/tests/unit/auth/test_identity_common.py index 1f88250c8..3e8cc2b81 100644 --- a/keystoneclient/tests/unit/auth/test_identity_common.py +++ b/keystoneclient/tests/unit/auth/test_identity_common.py @@ -12,21 +12,22 @@ import abc import datetime +from unittest import mock import uuid +from keystoneauth1 import fixture +from keystoneauth1 import plugin from oslo_utils import timeutils -import six from keystoneclient import access from keystoneclient.auth import base from keystoneclient.auth import identity -from keystoneclient import fixture +from keystoneclient import exceptions from keystoneclient import session from keystoneclient.tests.unit import utils -@six.add_metaclass(abc.ABCMeta) -class CommonIdentityTests(object): +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/' @@ -39,6 +40,7 @@ class CommonIdentityTests(object): 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) @@ -53,6 +55,7 @@ def create_auth_plugin(self, **kwargs): 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): @@ -61,6 +64,7 @@ def get_auth_data(self, **kwargs): 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) @@ -73,7 +77,8 @@ def stub_auth_data(self, **kwargs): self.stub_auth(json=token) - @abc.abstractproperty + @property + @abc.abstractmethod def version(self): """The API version being tested.""" @@ -186,7 +191,7 @@ def test_asking_for_auth_endpoint_ignores_checks(self): s = session.Session(auth=a) auth_url = s.get_endpoint(service_type='compute', - interface=base.AUTH_INTERFACE) + interface=plugin.AUTH_INTERFACE) self.assertEqual(self.TEST_URL, auth_url) @@ -312,6 +317,10 @@ class CatalogHackTests(utils.TestCase): 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', @@ -393,7 +402,7 @@ def test_getting_endpoints_on_auth_interface(self): sess = session.Session(auth=v2_auth) - endpoint = sess.get_endpoint(interface=base.AUTH_INTERFACE, + endpoint = sess.get_endpoint(interface=plugin.AUTH_INTERFACE, version=(3, 0)) self.assertEqual(self.V3_URL, endpoint) @@ -411,6 +420,9 @@ def __init__(self): 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) @@ -424,6 +436,9 @@ def get_headers(self, session, **kwargs): def get_endpoint(self, session, **kwargs): return self.endpoint + def get_connection_params(self, session, **kwargs): + return self.connection_params + class GenericAuthPluginTests(utils.TestCase): @@ -433,7 +448,9 @@ class GenericAuthPluginTests(utils.TestCase): def setUp(self): super(GenericAuthPluginTests, self).setUp() self.auth = GenericPlugin() - self.session = session.Session(auth=self.auth) + + with self.deprecations.expect_deprecations_here(): + self.session = session.Session(auth=self.auth) def test_setting_headers(self): text = uuid.uuid4().hex @@ -443,11 +460,45 @@ def test_setting_headers(self): self.assertEqual(text, resp.text) - for k, v in six.iteritems(self.auth.headers): + for k, v in self.auth.headers.items(): self.assertRequestHeaderEqual(k, v) - self.assertIsNone(self.session.get_token()) + 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 index 8eaae5450..84f4f5194 100644 --- a/keystoneclient/tests/unit/auth/test_identity_v2.py +++ b/keystoneclient/tests/unit/auth/test_identity_v2.py @@ -12,9 +12,9 @@ import argparse import copy +from unittest import mock import uuid -import mock from keystoneclient.auth.identity import v2 from keystoneclient import exceptions @@ -80,10 +80,11 @@ class V2IdentityPlugin(utils.TestCase): def setUp(self): super(V2IdentityPlugin, self).setUp() + self.deprecations.expect_deprecations() self.TEST_RESPONSE_DICT = { "access": { "token": { - "expires": "2020-01-01T00:00:10.000123Z", + "expires": "2999-01-01T00:00:10.000123Z", "id": self.TEST_TOKEN, "tenant": { "id": self.TEST_TENANT_ID @@ -275,11 +276,13 @@ def test_invalidate_response(self): password=self.TEST_PASS) s = session.Session(auth=a) - self.assertEqual('token1', s.get_token()) + with self.deprecations.expect_deprecations_here(): + self.assertEqual('token1', s.get_token()) self.assertEqual({'X-Auth-Token': 'token1'}, s.get_auth_headers()) a.invalidate() - self.assertEqual('token2', s.get_token()) + 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): @@ -289,7 +292,8 @@ def test_doesnt_log_password(self): a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=password) s = session.Session(auth=a) - self.assertEqual(self.TEST_TOKEN, s.get_token()) + 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) diff --git a/keystoneclient/tests/unit/auth/test_identity_v3.py b/keystoneclient/tests/unit/auth/test_identity_v3.py index 7e3eff8eb..fe7815ed2 100644 --- a/keystoneclient/tests/unit/auth/test_identity_v3.py +++ b/keystoneclient/tests/unit/auth/test_identity_v3.py @@ -12,16 +12,16 @@ import argparse import copy +from unittest import mock import uuid -import mock +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 fixture from keystoneclient import session from keystoneclient.tests.unit import utils @@ -120,6 +120,8 @@ def setUp(self): self.TEST_DISCOVERY_RESPONSE = { 'versions': {'values': [fixture.V3Discovery(self.TEST_URL)]}} + self.deprecations.expect_deprecations() + self.TEST_RESPONSE_DICT = { "token": { "methods": [ @@ -127,7 +129,7 @@ def setUp(self): "password" ], - "expires_at": "2020-01-01T00:00:10.000123Z", + "expires_at": "2999-01-01T00:00:10.000123Z", "project": { "domain": { "id": self.TEST_DOMAIN_ID, @@ -216,7 +218,7 @@ def test_authenticate_with_username_password_unscoped(self): username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) - cs = client.Client(session=s, auth_url=self.TEST_URL) + 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 @@ -245,7 +247,7 @@ 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_DOMAIN_ID) + project_id=self.TEST_TENANT_ID) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, @@ -255,10 +257,10 @@ def test_authenticate_with_username_password_project_scoped(self): {'methods': ['password'], 'password': {'user': {'name': self.TEST_USER, 'password': self.TEST_PASS}}}, - 'scope': {'project': {'id': self.TEST_DOMAIN_ID}}}} + '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_DOMAIN_ID) + 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) @@ -458,10 +460,12 @@ def test_invalidate_response(self): password=self.TEST_PASS) s = session.Session(auth=a) - self.assertEqual('token1', s.get_token()) + with self.deprecations.expect_deprecations_here(): + self.assertEqual('token1', s.get_token()) self.assertEqual({'X-Auth-Token': 'token1'}, s.get_auth_headers()) a.invalidate() - self.assertEqual('token2', s.get_token()) + 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): @@ -471,7 +475,8 @@ def test_doesnt_log_password(self): a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=password) s = session.Session(a) - self.assertEqual(self.TEST_TOKEN, s.get_token()) + 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()) @@ -487,7 +492,7 @@ def test_sends_nocatalog(self): include_catalog=False) s = session.Session(auth=a) - s.get_token() + s.get_auth_headers() auth_url = self.TEST_URL + '/auth/tokens' self.assertEqual(auth_url, a.token_url) @@ -512,7 +517,8 @@ def test_unscoped_request(self): auth_ref = a.get_access(s) - self.assertFalse(auth_ref.scoped) + with self.deprecations.expect_deprecations_here(): + self.assertFalse(auth_ref.scoped) body = self.requests_mock.last_request.json() ident = body['auth']['identity'] diff --git a/keystoneclient/tests/unit/auth/test_identity_v3_federated.py b/keystoneclient/tests/unit/auth/test_identity_v3_federated.py index b0fa119b3..7cbb5ab1e 100644 --- a/keystoneclient/tests/unit/auth/test_identity_v3_federated.py +++ b/keystoneclient/tests/unit/auth/test_identity_v3_federated.py @@ -13,9 +13,10 @@ import copy import uuid +from keystoneauth1 import fixture + from keystoneclient import access from keystoneclient.auth.identity import v3 -from keystoneclient import fixture from keystoneclient import session from keystoneclient.tests.unit import utils @@ -35,6 +36,8 @@ class V3FederatedPlugin(utils.TestCase): 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) diff --git a/keystoneclient/tests/unit/auth/test_loading.py b/keystoneclient/tests/unit/auth/test_loading.py index f8ef3b75a..3c2689dd9 100644 --- a/keystoneclient/tests/unit/auth/test_loading.py +++ b/keystoneclient/tests/unit/auth/test_loading.py @@ -12,7 +12,6 @@ import uuid -import six from keystoneclient.tests.unit.auth import utils @@ -39,7 +38,7 @@ def _getter(opt): self.assertEqual(set(vals), set(called_opts)) - for k, v in six.iteritems(vals): + for k, v in vals.items(): # replace - to _ because it's the dest used to create kwargs self.assertEqual(v, p[k.replace('-', '_')]) diff --git a/keystoneclient/tests/unit/auth/test_password.py b/keystoneclient/tests/unit/auth/test_password.py index 7926a22e8..93d2fb8ca 100644 --- a/keystoneclient/tests/unit/auth/test_password.py +++ b/keystoneclient/tests/unit/auth/test_password.py @@ -11,9 +11,9 @@ # under the License. import argparse +from unittest import mock import uuid -import mock from keystoneclient.auth.identity.generic import password from keystoneclient.auth.identity import v2 @@ -46,7 +46,7 @@ def test_v3_user_params_v2_url(self): def test_options(self): opts = [o.name for o in self.PLUGIN_CLASS.get_options()] - allowed_opts = ['user-name', + allowed_opts = ['username', 'user-domain-id', 'user-domain-name', 'user-id', diff --git a/keystoneclient/tests/unit/auth/test_token_endpoint.py b/keystoneclient/tests/unit/auth/test_token_endpoint.py index b0be8f16b..9a9a1ed90 100644 --- a/keystoneclient/tests/unit/auth/test_token_endpoint.py +++ b/keystoneclient/tests/unit/auth/test_token_endpoint.py @@ -22,6 +22,10 @@ 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') diff --git a/keystoneclient/tests/unit/auth/utils.py b/keystoneclient/tests/unit/auth/utils.py index 87c2b62ff..1e346e35c 100644 --- a/keystoneclient/tests/unit/auth/utils.py +++ b/keystoneclient/tests/unit/auth/utils.py @@ -11,16 +11,15 @@ # under the License. import functools +from unittest import mock import uuid -import mock +from keystoneauth1 import fixture from oslo_config import cfg -import six from keystoneclient import access from keystoneclient.auth import base from keystoneclient import exceptions -from keystoneclient import fixture from keystoneclient import session from keystoneclient.tests.unit import utils @@ -37,6 +36,7 @@ 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): @@ -87,7 +87,7 @@ class TestCase(utils.TestCase): 'a_bool': a_bool} def assertTestVals(self, plugin, vals=TEST_VALS): - for k, v in six.iteritems(vals): + for k, v in vals.items(): self.assertEqual(v, plugin[k]) @@ -106,6 +106,8 @@ def setUp(self): 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) diff --git a/keystoneclient/tests/unit/client_fixtures.py b/keystoneclient/tests/unit/client_fixtures.py index b226e32bb..c6c291984 100644 --- a/keystoneclient/tests/unit/client_fixtures.py +++ b/keystoneclient/tests/unit/client_fixtures.py @@ -12,18 +12,29 @@ # 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 six 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') @@ -31,8 +42,168 @@ 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, six.text_type): + if isinstance(signed_text, str): signed_text = signed_text.encode('utf-8') return utils.hash_signed_token(signed_text, **kwargs) @@ -127,7 +298,7 @@ def setUp(self): self.v3_UUID_TOKEN_UNKNOWN_BIND = '7ed9781b62cd4880b8d8c6788ab1d1e2' revoked_token = self.REVOKED_TOKEN - if isinstance(revoked_token, six.text_type): + 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, @@ -138,7 +309,7 @@ def setUp(self): self.REVOKED_TOKEN_LIST_JSON = jsonutils.dumps(self.REVOKED_TOKEN_LIST) revoked_v3_token = self.REVOKED_v3_TOKEN - if isinstance(revoked_v3_token, six.text_type): + 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') @@ -150,12 +321,12 @@ def setUp(self): self.REVOKED_v3_TOKEN_LIST) revoked_token_pkiz = self.REVOKED_TOKEN_PKIZ - if isinstance(revoked_token_pkiz, six.text_type): + 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, six.text_type): + 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) @@ -227,7 +398,7 @@ def setUp(self): 'access': { 'token': { 'id': self.UUID_TOKEN_DEFAULT, - 'expires': '2020-01-01T00:00:10.000123Z', + 'expires': '2999-01-01T00:00:10.000123Z', 'tenant': { 'id': 'tenant_id1', 'name': 'tenant_name1', @@ -248,7 +419,7 @@ def setUp(self): 'access': { 'token': { 'id': self.VALID_DIABLO_TOKEN, - 'expires': '2020-01-01T00:00:10.000123Z', + 'expires': '2999-01-01T00:00:10.000123Z', 'tenantId': 'tenant_id1', }, 'user': { @@ -265,7 +436,7 @@ def setUp(self): 'access': { 'token': { 'id': self.UUID_TOKEN_UNSCOPED, - 'expires': '2020-01-01T00:00:10.000123Z', + 'expires': '2999-01-01T00:00:10.000123Z', }, 'user': { 'id': 'user_id1', @@ -281,7 +452,7 @@ def setUp(self): 'access': { 'token': { 'id': 'valid-token', - 'expires': '2020-01-01T00:00:10.000123Z', + 'expires': '2999-01-01T00:00:10.000123Z', 'tenant': { 'id': 'tenant_id1', 'name': 'tenant_name1', @@ -302,7 +473,7 @@ def setUp(self): 'token': { 'bind': {'kerberos': self.KERBEROS_BIND}, 'id': self.UUID_TOKEN_BIND, - 'expires': '2020-01-01T00:00:10.000123Z', + 'expires': '2999-01-01T00:00:10.000123Z', 'tenant': { 'id': 'tenant_id1', 'name': 'tenant_name1', @@ -324,7 +495,7 @@ def setUp(self): 'token': { 'bind': {'FOO': 'BAR'}, 'id': self.UUID_TOKEN_UNKNOWN_BIND, - 'expires': '2020-01-01T00:00:10.000123Z', + 'expires': '2999-01-01T00:00:10.000123Z', 'tenant': { 'id': 'tenant_id1', 'name': 'tenant_name1', @@ -343,7 +514,7 @@ def setUp(self): }, self.v3_UUID_TOKEN_DEFAULT: { 'token': { - 'expires_at': '2020-01-01T00:00:10.000123Z', + 'expires_at': '2999-01-01T00:00:10.000123Z', 'methods': ['password'], 'user': { 'id': 'user_id1', @@ -370,7 +541,7 @@ def setUp(self): }, self.v3_UUID_TOKEN_UNSCOPED: { 'token': { - 'expires_at': '2020-01-01T00:00:10.000123Z', + 'expires_at': '2999-01-01T00:00:10.000123Z', 'methods': ['password'], 'user': { 'id': 'user_id1', @@ -384,7 +555,7 @@ def setUp(self): }, self.v3_UUID_TOKEN_DOMAIN_SCOPED: { 'token': { - 'expires_at': '2020-01-01T00:00:10.000123Z', + 'expires_at': '2999-01-01T00:00:10.000123Z', 'methods': ['password'], 'user': { 'id': 'user_id1', @@ -409,7 +580,7 @@ def setUp(self): 'access': { 'token': { 'id': self.SIGNED_TOKEN_SCOPED_KEY, - 'expires': '2020-01-01T00:00:10.000123Z', + 'expires': '2999-01-01T00:00:10.000123Z', }, 'user': { 'id': 'user_id1', @@ -427,7 +598,7 @@ def setUp(self): 'access': { 'token': { 'id': self.SIGNED_TOKEN_UNSCOPED_KEY, - 'expires': '2020-01-01T00:00:10.000123Z', + 'expires': '2999-01-01T00:00:10.000123Z', }, 'user': { 'id': 'user_id1', @@ -441,7 +612,7 @@ def setUp(self): }, self.SIGNED_v3_TOKEN_SCOPED_KEY: { 'token': { - 'expires_at': '2020-01-01T00:00:10.000123Z', + 'expires_at': '2999-01-01T00:00:10.000123Z', 'methods': ['password'], 'user': { 'id': 'user_id1', @@ -470,7 +641,7 @@ def setUp(self): 'token': { 'bind': {'kerberos': self.KERBEROS_BIND}, 'methods': ['password'], - 'expires_at': '2020-01-01T00:00:10.000123Z', + 'expires_at': '2999-01-01T00:00:10.000123Z', 'user': { 'id': 'user_id1', 'name': 'user_name1', @@ -497,7 +668,7 @@ def setUp(self): self.v3_UUID_TOKEN_UNKNOWN_BIND: { 'token': { 'bind': {'FOO': 'BAR'}, - 'expires_at': '2020-01-01T00:00:10.000123Z', + 'expires_at': '2999-01-01T00:00:10.000123Z', 'methods': ['password'], 'user': { 'id': 'user_id1', @@ -531,67 +702,42 @@ def setUp(self): self.TOKEN_RESPONSES[self.SIGNED_v3_TOKEN_SCOPED_KEY]) self.JSON_TOKEN_RESPONSES = dict([(k, jsonutils.dumps(v)) for k, v in - six.iteritems(self.TOKEN_RESPONSES)]) + self.TOKEN_RESPONSES.items()]) EXAMPLES_RESOURCE = testresources.FixtureResource(Examples()) -class HackingCode(fixtures.Fixture): - """A fixture to house the various code examples for the keystoneclient - hacking style checks. - """ - - oslo_namespace_imports = { - 'code': """ - import oslo.utils - import oslo_utils - import oslo.utils.encodeutils - import oslo_utils.encodeutils - from oslo import utils - from oslo.utils import encodeutils - from oslo_utils import encodeutils - - import oslo.serialization - import oslo_serialization - import oslo.serialization.jsonutils - import oslo_serialization.jsonutils - from oslo import serialization - from oslo.serialization import jsonutils - from oslo_serialization import jsonutils - - import oslo.config - import oslo_config - import oslo.config.cfg - import oslo_config.cfg - from oslo import config - from oslo.config import cfg - from oslo_config import cfg - - import oslo.i18n - import oslo_i18n - import oslo.i18n.log - import oslo_i18n.log - from oslo import i18n - from oslo.i18n import log - from oslo_i18n import log - """, - 'expected_errors': [ - (1, 0, 'K333'), - (3, 0, 'K333'), - (5, 0, 'K333'), - (6, 0, 'K333'), - (9, 0, 'K333'), - (11, 0, 'K333'), - (13, 0, 'K333'), - (14, 0, 'K333'), - (17, 0, 'K333'), - (19, 0, 'K333'), - (21, 0, 'K333'), - (22, 0, 'K333'), - (25, 0, 'K333'), - (27, 0, 'K333'), - (29, 0, 'K333'), - (30, 0, 'K333'), - ], - } +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/test_client.py b/keystoneclient/tests/unit/generic/test_client.py index 6eb6836e5..5c27b6eb9 100644 --- a/keystoneclient/tests/unit/generic/test_client.py +++ b/keystoneclient/tests/unit/generic/test_client.py @@ -22,7 +22,8 @@ BASE_URL = "%s:5000/" % BASE_HOST V2_URL = "%sv2.0" % BASE_URL -EXTENSION_NAMESPACE = "http://docs.openstack.org/identity/api/ext/OS-FAKE/v1.0" +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"} @@ -57,7 +58,10 @@ class ClientDiscoveryTests(utils.TestCase): def test_discover_extensions_v2(self): self.requests_mock.get("%s/extensions" % V2_URL, text=EXTENSION_LIST) - extensions = client.Client().discover_extensions(url=V2_URL) + # 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) diff --git a/keystoneclient/tests/unit/generic/test_shell.py b/keystoneclient/tests/unit/generic/test_shell.py deleted file mode 100644 index ad457aad3..000000000 --- a/keystoneclient/tests/unit/generic/test_shell.py +++ /dev/null @@ -1,129 +0,0 @@ -# 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. - -import mock -from six import moves - -from keystoneclient.generic import shell -from keystoneclient.tests.unit import utils - - -class DoDiscoverTest(utils.TestCase): - """Unit tests for do_discover function.""" - foo_version = { - 'id': 'foo_id', - 'status': 'foo_status', - 'url': 'http://foo/url', - } - bar_version = { - 'id': 'bar_id', - 'status': 'bar_status', - 'url': 'http://bar/url', - } - foo_extension = { - 'foo': 'foo_extension', - 'message': 'extension_message', - 'bar': 'bar_extension', - } - stub_message = 'This is a stub message' - - def setUp(self): - super(DoDiscoverTest, self).setUp() - - self.client_mock = mock.Mock() - self.client_mock.discover.return_value = {} - - def _execute_discover(self): - """Call do_discover function and capture output - - :returns: captured output is returned - """ - with mock.patch('sys.stdout', - new_callable=moves.StringIO) as mock_stdout: - shell.do_discover(self.client_mock, args=None) - output = mock_stdout.getvalue() - return output - - def _check_version_print(self, output, version): - """Checks all api version's parameters are present in output.""" - self.assertIn(version['id'], output) - self.assertIn(version['status'], output) - self.assertIn(version['url'], output) - - def test_no_keystones(self): - # No servers configured for client, - # corresponding message should be printed - output = self._execute_discover() - self.assertIn('No Keystone-compatible endpoint found', output) - - def test_endpoint(self): - # Endpoint is configured for client, - # client's discover method should be called with that value - self.client_mock.endpoint = 'Some non-empty value' - shell.do_discover(self.client_mock, args=None) - self.client_mock.discover.assert_called_with(self.client_mock.endpoint) - - def test_auth_url(self): - # No endpoint provided for client, but there is an auth_url - # client's discover method should be called with auth_url value - self.client_mock.endpoint = False - self.client_mock.auth_url = 'Some non-empty value' - shell.do_discover(self.client_mock, args=None) - self.client_mock.discover.assert_called_with(self.client_mock.auth_url) - - def test_empty(self): - # No endpoint or auth_url is configured for client. - # client.discover() should be called without parameters - self.client_mock.endpoint = False - self.client_mock.auth_url = False - shell.do_discover(self.client_mock, args=None) - self.client_mock.discover.assert_called_with() - - def test_message(self): - # If client.discover() result contains message - it should be printed - self.client_mock.discover.return_value = {'message': self.stub_message} - output = self._execute_discover() - self.assertIn(self.stub_message, output) - - def test_versions(self): - # Every version in client.discover() result should be printed - # and client.discover_extension() should be called on its url - self.client_mock.discover.return_value = { - 'foo': self.foo_version, - 'bar': self.bar_version, - } - self.client_mock.discover_extensions.return_value = {} - output = self._execute_discover() - self._check_version_print(output, self.foo_version) - self._check_version_print(output, self.bar_version) - - discover_extension_calls = [ - mock.call(self.foo_version['url']), - mock.call(self.bar_version['url']), - ] - - self.client_mock.discover_extensions.assert_has_calls( - discover_extension_calls, - any_order=True) - - def test_extensions(self): - # Every extension's parameters should be printed - # Extension's message should be omitted - self.client_mock.discover.return_value = {'foo': self.foo_version} - self.client_mock.discover_extensions.return_value = self.foo_extension - output = self._execute_discover() - self.assertIn(self.foo_extension['foo'], output) - self.assertIn(self.foo_extension['bar'], output) - self.assertNotIn(self.foo_extension['message'], output) diff --git a/keystoneclient/tests/unit/test_auth_token_middleware.py b/keystoneclient/tests/unit/test_auth_token_middleware.py deleted file mode 100644 index 091f95616..000000000 --- a/keystoneclient/tests/unit/test_auth_token_middleware.py +++ /dev/null @@ -1,1945 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import calendar -import datetime -import json -import os -import shutil -import stat -import tempfile -import time -import uuid - -import fixtures -import iso8601 -import mock -from oslo_serialization import jsonutils -from oslo_utils import timeutils -from requests_mock.contrib import fixture as mock_fixture -import six -from six.moves.urllib import parse as urlparse -import testresources -import testtools -from testtools import matchers -import webob - -from keystoneclient import access -from keystoneclient.common import cms -from keystoneclient import exceptions -from keystoneclient import fixture -from keystoneclient.middleware import auth_token -from keystoneclient.openstack.common import memorycache -from keystoneclient.tests.unit import client_fixtures -from keystoneclient.tests.unit import utils - - -EXPECTED_V2_DEFAULT_ENV_RESPONSE = { - 'HTTP_X_IDENTITY_STATUS': 'Confirmed', - 'HTTP_X_TENANT_ID': 'tenant_id1', - 'HTTP_X_TENANT_NAME': 'tenant_name1', - 'HTTP_X_USER_ID': 'user_id1', - 'HTTP_X_USER_NAME': 'user_name1', - 'HTTP_X_ROLES': 'role1,role2', - 'HTTP_X_USER': 'user_name1', # deprecated (diablo-compat) - 'HTTP_X_TENANT': 'tenant_name1', # deprecated (diablo-compat) - 'HTTP_X_ROLE': 'role1,role2', # deprecated (diablo-compat) -} - - -BASE_HOST = 'https://keystone.example.com:1234' -BASE_URI = '%s/testadmin' % BASE_HOST -FAKE_ADMIN_TOKEN_ID = 'admin_token2' -FAKE_ADMIN_TOKEN = jsonutils.dumps( - {'access': {'token': {'id': FAKE_ADMIN_TOKEN_ID, - 'expires': '2022-10-03T16:58:01Z'}}}) - - -VERSION_LIST_v2 = jsonutils.dumps(fixture.DiscoveryList(href=BASE_URI, - v3=False)) -VERSION_LIST_v3 = jsonutils.dumps(fixture.DiscoveryList(href=BASE_URI)) - -ERROR_TOKEN = '7ae290c2a06244c4b41692eb4e9225f2' -MEMCACHED_SERVERS = ['localhost:11211'] -MEMCACHED_AVAILABLE = None - - -def memcached_available(): - """Do a sanity check against memcached. - - Returns ``True`` if the following conditions are met (otherwise, returns - ``False``): - - - ``python-memcached`` is installed - - a usable ``memcached`` instance is available via ``MEMCACHED_SERVERS`` - - the client is able to set and get a key/value pair - - """ - global MEMCACHED_AVAILABLE - - if MEMCACHED_AVAILABLE is None: - try: - import memcache - c = memcache.Client(MEMCACHED_SERVERS) - c.set('ping', 'pong', time=1) - MEMCACHED_AVAILABLE = c.get('ping') == 'pong' - except ImportError: - MEMCACHED_AVAILABLE = False - - return MEMCACHED_AVAILABLE - - -def cleanup_revoked_file(filename): - try: - os.remove(filename) - except OSError: - pass - - -class TimezoneFixture(fixtures.Fixture): - @staticmethod - def supported(): - # tzset is only supported on Unix. - return hasattr(time, 'tzset') - - def __init__(self, new_tz): - super(TimezoneFixture, self).__init__() - self.tz = new_tz - self.old_tz = os.environ.get('TZ') - - def setUp(self): - super(TimezoneFixture, self).setUp() - if not self.supported(): - raise NotImplementedError('timezone override is not supported.') - os.environ['TZ'] = self.tz - time.tzset() - self.addCleanup(self.cleanup) - - def cleanup(self): - if self.old_tz is not None: - os.environ['TZ'] = self.old_tz - elif 'TZ' in os.environ: - del os.environ['TZ'] - time.tzset() - - -class TimeFixture(fixtures.Fixture): - - def __init__(self, new_time, normalize=True): - super(TimeFixture, self).__init__() - if isinstance(new_time, six.string_types): - new_time = timeutils.parse_isotime(new_time) - if normalize: - new_time = timeutils.normalize_time(new_time) - self.new_time = new_time - - def setUp(self): - super(TimeFixture, self).setUp() - timeutils.set_time_override(self.new_time) - self.addCleanup(timeutils.clear_time_override) - - -class FakeApp(object): - """This represents a WSGI app protected by the auth_token middleware.""" - - SUCCESS = b'SUCCESS' - - def __init__(self, expected_env=None): - self.expected_env = dict(EXPECTED_V2_DEFAULT_ENV_RESPONSE) - - if expected_env: - 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 = FakeApp.SUCCESS - return resp(env, start_response) - - -class v3FakeApp(FakeApp): - """This represents a v3 WSGI app protected by the auth_token middleware.""" - - def __init__(self, expected_env=None): - - # with v3 additions, these are for the DEFAULT TOKEN - v3_default_env_additions = { - 'HTTP_X_PROJECT_ID': 'tenant_id1', - 'HTTP_X_PROJECT_NAME': 'tenant_name1', - 'HTTP_X_PROJECT_DOMAIN_ID': 'domain_id1', - 'HTTP_X_PROJECT_DOMAIN_NAME': 'domain_name1', - 'HTTP_X_USER_DOMAIN_ID': 'domain_id1', - 'HTTP_X_USER_DOMAIN_NAME': 'domain_name1' - } - - if expected_env: - v3_default_env_additions.update(expected_env) - - super(v3FakeApp, self).__init__(v3_default_env_additions) - - -class BaseAuthTokenMiddlewareTest(testtools.TestCase): - """Base test class for auth_token middleware. - - All the tests allow for running with auth_token - configured for receiving v2 or v3 tokens, with the - choice being made by passing configuration data into - setUp(). - - The base class will, by default, run all the tests - expecting v2 token formats. Child classes can override - this to specify, for instance, v3 format. - - """ - def setUp(self, expected_env=None, auth_version=None, fake_app=None): - testtools.TestCase.setUp(self) - - self.expected_env = expected_env or dict() - self.fake_app = fake_app or FakeApp - self.middleware = None - - self.conf = { - 'identity_uri': 'https://keystone.example.com:1234/testadmin/', - 'signing_dir': client_fixtures.CERTDIR, - 'auth_version': auth_version, - 'auth_uri': 'https://keystone.example.com:1234', - } - - self.auth_version = auth_version - self.response_status = None - self.response_headers = None - - self.requests_mock = self.useFixture(mock_fixture.Fixture()) - - def set_middleware(self, expected_env=None, conf=None): - """Configure the class ready to call the auth_token middleware. - - Set up the various fake items needed to run the middleware. - Individual tests that need to further refine these can call this - function to override the class defaults. - - """ - if conf: - self.conf.update(conf) - - if expected_env: - self.expected_env.update(expected_env) - - self.middleware = auth_token.AuthProtocol( - self.fake_app(self.expected_env), self.conf) - self.middleware._iso8601 = iso8601 - - with tempfile.NamedTemporaryFile(dir=self.middleware.signing_dirname, - delete=False) as f: - pass - self.middleware.revoked_file_name = f.name - - self.addCleanup(cleanup_revoked_file, - self.middleware.revoked_file_name) - - self.middleware.token_revocation_list = jsonutils.dumps( - {"revoked": [], "extra": "success"}) - - def start_fake_response(self, status, headers): - self.response_status = int(status.split(' ', 1)[0]) - self.response_headers = dict(headers) - - def assertLastPath(self, path): - if path: - parts = urlparse.urlparse(self.requests_mock.last_request.url) - self.assertEqual(path, parts.path) - else: - self.assertIsNone(self.requests_mock.last_request) - - -class MultiStepAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, - testresources.ResourcedTestCase): - - resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] - - def test_fetch_revocation_list_with_expire(self): - self.set_middleware() - - # Get a token, then try to retrieve revocation list and get a 401. - # Get a new token, try to retrieve revocation list and return 200. - self.requests_mock.post("%s/v2.0/tokens" % BASE_URI, - text=FAKE_ADMIN_TOKEN) - - text = self.examples.SIGNED_REVOCATION_LIST - self.requests_mock.get("%s/v2.0/tokens/revoked" % BASE_URI, - response_list=[{'status_code': 401}, - {'text': text}]) - - fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list()) - self.assertEqual(fetched_list, self.examples.REVOCATION_LIST) - - # Check that 4 requests have been made - self.assertEqual(len(self.requests_mock.request_history), 4) - - -class DiabloAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, - testresources.ResourcedTestCase): - - resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] - - """Auth Token middleware should understand Diablo keystone responses.""" - def setUp(self): - # pre-diablo only had Tenant ID, which was also the Name - expected_env = { - 'HTTP_X_TENANT_ID': 'tenant_id1', - 'HTTP_X_TENANT_NAME': 'tenant_id1', - # now deprecated (diablo-compat) - 'HTTP_X_TENANT': 'tenant_id1', - } - - super(DiabloAuthTokenMiddlewareTest, self).setUp( - expected_env=expected_env) - - self.requests_mock.get("%s/" % BASE_URI, - text=VERSION_LIST_v2, - status_code=300) - - self.requests_mock.post("%s/v2.0/tokens" % BASE_URI, - text=FAKE_ADMIN_TOKEN) - - self.token_id = self.examples.VALID_DIABLO_TOKEN - token_response = self.examples.JSON_TOKEN_RESPONSES[self.token_id] - - url = '%s/v2.0/tokens/%s' % (BASE_URI, self.token_id) - self.requests_mock.get(url, text=token_response) - - self.set_middleware() - - def test_valid_diablo_response(self): - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = self.token_id - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 200) - self.assertIn('keystone.token_info', req.environ) - - -class NoMemcacheAuthToken(BaseAuthTokenMiddlewareTest): - """These tests will not have the memcache module available.""" - - def setUp(self): - super(NoMemcacheAuthToken, self).setUp() - self.useFixture(utils.DisableModuleFixture('memcache')) - - def test_nomemcache(self): - conf = { - 'admin_token': 'admin_token1', - 'auth_host': 'keystone.example.com', - 'auth_port': 1234, - 'memcached_servers': MEMCACHED_SERVERS, - 'auth_uri': 'https://keystone.example.com:1234', - } - - auth_token.AuthProtocol(FakeApp(), conf) - - -class CachePoolTest(BaseAuthTokenMiddlewareTest): - def test_use_cache_from_env(self): - """If `swift.cache` is set in the environment and `cache` is set in the - config then the env cache is used. - """ - env = {'swift.cache': 'CACHE_TEST'} - conf = { - 'cache': 'swift.cache' - } - self.set_middleware(conf=conf) - self.middleware._token_cache.initialize(env) - with self.middleware._token_cache._cache_pool.reserve() as cache: - self.assertEqual(cache, 'CACHE_TEST') - - def test_not_use_cache_from_env(self): - """If `swift.cache` is set in the environment but `cache` isn't set in - the config then the env cache isn't used. - """ - self.set_middleware() - env = {'swift.cache': 'CACHE_TEST'} - self.middleware._token_cache.initialize(env) - with self.middleware._token_cache._cache_pool.reserve() as cache: - self.assertNotEqual(cache, 'CACHE_TEST') - - def test_multiple_context_managers_share_single_client(self): - self.set_middleware() - token_cache = self.middleware._token_cache - env = {} - token_cache.initialize(env) - - caches = [] - - with token_cache._cache_pool.reserve() as cache: - caches.append(cache) - - with token_cache._cache_pool.reserve() as cache: - caches.append(cache) - - self.assertIs(caches[0], caches[1]) - self.assertEqual(set(caches), set(token_cache._cache_pool)) - - def test_nested_context_managers_create_multiple_clients(self): - self.set_middleware() - env = {} - self.middleware._token_cache.initialize(env) - token_cache = self.middleware._token_cache - - with token_cache._cache_pool.reserve() as outer_cache: - with token_cache._cache_pool.reserve() as inner_cache: - self.assertNotEqual(outer_cache, inner_cache) - - self.assertEqual( - set([inner_cache, outer_cache]), - set(token_cache._cache_pool)) - - -class GeneralAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, - testresources.ResourcedTestCase): - """These tests are not affected by the token format - (see CommonAuthTokenMiddlewareTest). - """ - - resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] - - def test_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_token_is_v2_accepts_v2(self): - token = self.examples.UUID_TOKEN_DEFAULT - token_response = self.examples.TOKEN_RESPONSES[token] - self.assertTrue(auth_token._token_is_v2(token_response)) - - def test_token_is_v2_rejects_v3(self): - token = self.examples.v3_UUID_TOKEN_DEFAULT - token_response = self.examples.TOKEN_RESPONSES[token] - self.assertFalse(auth_token._token_is_v2(token_response)) - - def test_token_is_v3_rejects_v2(self): - token = self.examples.UUID_TOKEN_DEFAULT - token_response = self.examples.TOKEN_RESPONSES[token] - self.assertFalse(auth_token._token_is_v3(token_response)) - - def test_token_is_v3_accepts_v3(self): - token = self.examples.v3_UUID_TOKEN_DEFAULT - token_response = self.examples.TOKEN_RESPONSES[token] - self.assertTrue(auth_token._token_is_v3(token_response)) - - @testtools.skipUnless(memcached_available(), 'memcached not available') - def test_encrypt_cache_data(self): - conf = { - 'memcached_servers': MEMCACHED_SERVERS, - 'memcache_security_strategy': 'encrypt', - 'memcache_secret_key': 'mysecret' - } - self.set_middleware(conf=conf) - token = b'my_token' - some_time_later = timeutils.utcnow() + datetime.timedelta(hours=4) - expires = timeutils.strtime(some_time_later) - data = ('this_data', expires) - token_cache = self.middleware._token_cache - token_cache.initialize({}) - token_cache._cache_store(token, data) - self.assertEqual(token_cache._cache_get(token), data[0]) - - @testtools.skipUnless(memcached_available(), 'memcached not available') - def test_sign_cache_data(self): - conf = { - 'memcached_servers': MEMCACHED_SERVERS, - 'memcache_security_strategy': 'mac', - 'memcache_secret_key': 'mysecret' - } - self.set_middleware(conf=conf) - token = b'my_token' - some_time_later = timeutils.utcnow() + datetime.timedelta(hours=4) - expires = timeutils.strtime(some_time_later) - data = ('this_data', expires) - token_cache = self.middleware._token_cache - token_cache.initialize({}) - token_cache._cache_store(token, data) - self.assertEqual(token_cache._cache_get(token), data[0]) - - @testtools.skipUnless(memcached_available(), 'memcached not available') - def test_no_memcache_protection(self): - conf = { - 'memcached_servers': MEMCACHED_SERVERS, - 'memcache_secret_key': 'mysecret' - } - self.set_middleware(conf=conf) - token = 'my_token' - some_time_later = timeutils.utcnow() + datetime.timedelta(hours=4) - expires = timeutils.strtime(some_time_later) - data = ('this_data', expires) - token_cache = self.middleware._token_cache - token_cache.initialize({}) - token_cache._cache_store(token, data) - self.assertEqual(token_cache._cache_get(token), data[0]) - - def test_assert_valid_memcache_protection_config(self): - # test missing memcache_secret_key - conf = { - 'memcached_servers': MEMCACHED_SERVERS, - 'memcache_security_strategy': 'Encrypt' - } - self.assertRaises(auth_token.ConfigurationError, self.set_middleware, - conf=conf) - # test invalue memcache_security_strategy - conf = { - 'memcached_servers': MEMCACHED_SERVERS, - 'memcache_security_strategy': 'whatever' - } - self.assertRaises(auth_token.ConfigurationError, self.set_middleware, - conf=conf) - # test missing memcache_secret_key - conf = { - 'memcached_servers': MEMCACHED_SERVERS, - 'memcache_security_strategy': 'mac' - } - self.assertRaises(auth_token.ConfigurationError, self.set_middleware, - conf=conf) - conf = { - 'memcached_servers': MEMCACHED_SERVERS, - 'memcache_security_strategy': 'Encrypt', - 'memcache_secret_key': '' - } - self.assertRaises(auth_token.ConfigurationError, self.set_middleware, - conf=conf) - conf = { - 'memcached_servers': MEMCACHED_SERVERS, - 'memcache_security_strategy': 'mAc', - 'memcache_secret_key': '' - } - self.assertRaises(auth_token.ConfigurationError, self.set_middleware, - conf=conf) - - def test_config_revocation_cache_timeout(self): - conf = { - 'revocation_cache_time': 24, - 'auth_uri': 'https://keystone.example.com:1234', - } - middleware = auth_token.AuthProtocol(self.fake_app, conf) - self.assertEqual(middleware.token_revocation_list_cache_timeout, - datetime.timedelta(seconds=24)) - - def test_conf_values_type_convert(self): - conf = { - 'revocation_cache_time': '24', - 'identity_uri': 'https://keystone.example.com:1234', - 'include_service_catalog': '0', - 'nonexsit_option': '0', - } - - middleware = auth_token.AuthProtocol(self.fake_app, conf) - self.assertEqual(datetime.timedelta(seconds=24), - middleware.token_revocation_list_cache_timeout) - self.assertEqual(False, middleware.include_service_catalog) - self.assertEqual('https://keystone.example.com:1234', - middleware.identity_uri) - self.assertEqual('0', middleware.conf['nonexsit_option']) - - def test_conf_values_type_convert_with_wrong_value(self): - conf = { - 'include_service_catalog': '123', - } - self.assertRaises(auth_token.ConfigurationError, - auth_token.AuthProtocol, self.fake_app, conf) - - -class CommonAuthTokenMiddlewareTest(object): - """These tests are run once using v2 tokens and again using v3 tokens.""" - - def test_init_does_not_call_http(self): - conf = { - 'revocation_cache_time': 1 - } - self.set_middleware(conf=conf) - self.assertLastPath(None) - - def test_init_by_ipv6Addr_auth_host(self): - del self.conf['identity_uri'] - conf = { - 'auth_host': '2001:2013:1:f101::1', - 'auth_port': 1234, - 'auth_protocol': 'http', - 'auth_uri': None, - } - self.set_middleware(conf=conf) - expected_auth_uri = 'http://[2001:2013:1:f101::1]:1234' - self.assertEqual(expected_auth_uri, self.middleware.auth_uri) - - def assert_valid_request_200(self, token, with_catalog=True): - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = token - body = self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 200) - if with_catalog: - self.assertTrue(req.headers.get('X-Service-Catalog')) - else: - self.assertNotIn('X-Service-Catalog', req.headers) - self.assertEqual(body, [FakeApp.SUCCESS]) - self.assertIn('keystone.token_info', req.environ) - return req - - def test_valid_uuid_request(self): - for _ in range(2): # Do it twice because first result was cached. - token = self.token_dict['uuid_token_default'] - self.assert_valid_request_200(token) - self.assert_valid_last_url(token) - - def test_valid_uuid_request_with_auth_fragments(self): - del self.conf['identity_uri'] - self.conf['auth_protocol'] = 'https' - self.conf['auth_host'] = 'keystone.example.com' - self.conf['auth_port'] = 1234 - self.conf['auth_admin_prefix'] = '/testadmin' - self.set_middleware() - self.assert_valid_request_200(self.token_dict['uuid_token_default']) - self.assert_valid_last_url(self.token_dict['uuid_token_default']) - - def _test_cache_revoked(self, token, revoked_form=None): - # When the token is cached and revoked, 401 is returned. - self.middleware.check_revocations_for_cached = True - - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = token - - # Token should be cached as ok after this. - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(200, self.response_status) - - # Put it in revocation list. - self.middleware.token_revocation_list = self.get_revocation_list_json( - token_ids=[revoked_form or token]) - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(401, self.response_status) - - def test_cached_revoked_uuid(self): - # When the UUID token is cached and revoked, 401 is returned. - self._test_cache_revoked(self.token_dict['uuid_token_default']) - - def test_valid_signed_request(self): - for _ in range(2): # Do it twice because first result was cached. - self.assert_valid_request_200( - self.token_dict['signed_token_scoped']) - # ensure that signed requests do not generate HTTP traffic - self.assertLastPath(None) - - def test_valid_signed_compressed_request(self): - self.assert_valid_request_200( - self.token_dict['signed_token_scoped_pkiz']) - # ensure that signed requests do not generate HTTP traffic - self.assertLastPath(None) - - def test_revoked_token_receives_401(self): - self.middleware.token_revocation_list = self.get_revocation_list_json() - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = self.token_dict['revoked_token'] - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - - def test_revoked_token_receives_401_sha256(self): - self.conf['hash_algorithms'] = ['sha256', 'md5'] - self.set_middleware() - self.middleware.token_revocation_list = ( - self.get_revocation_list_json(mode='sha256')) - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = self.token_dict['revoked_token'] - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - - def test_cached_revoked_pki(self): - # When the PKI token is cached and revoked, 401 is returned. - token = self.token_dict['signed_token_scoped'] - revoked_form = cms.cms_hash_token(token) - self._test_cache_revoked(token, revoked_form) - - def test_cached_revoked_pkiz(self): - # When the PKI token is cached and revoked, 401 is returned. - token = self.token_dict['signed_token_scoped_pkiz'] - revoked_form = cms.cms_hash_token(token) - self._test_cache_revoked(token, revoked_form) - - def test_revoked_token_receives_401_md5_secondary(self): - # When hash_algorithms has 'md5' as the secondary hash and the - # revocation list contains the md5 hash for a token, that token is - # considered revoked so returns 401. - self.conf['hash_algorithms'] = ['sha256', 'md5'] - self.set_middleware() - self.middleware.token_revocation_list = self.get_revocation_list_json() - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = self.token_dict['revoked_token'] - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - - def _test_revoked_hashed_token(self, token_key): - # If hash_algorithms is set as ['sha256', 'md5'], - # and check_revocations_for_cached is True, - # and a token is in the cache because it was successfully validated - # using the md5 hash, then - # if the token is in the revocation list by md5 hash, it'll be - # rejected and auth_token returns 401. - self.conf['hash_algorithms'] = ['sha256', 'md5'] - self.conf['check_revocations_for_cached'] = True - self.set_middleware() - - token = self.token_dict[token_key] - - # Put the token in the revocation list. - token_hashed = cms.cms_hash_token(token) - self.middleware.token_revocation_list = self.get_revocation_list_json( - token_ids=[token_hashed]) - - # request is using the hashed token, is valid so goes in - # cache using the given hash. - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = token_hashed - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(200, self.response_status) - - # This time use the PKI(Z) token - req.headers['X-Auth-Token'] = token - self.middleware(req.environ, self.start_fake_response) - - # Should find the token in the cache and revocation list. - self.assertEqual(401, self.response_status) - - def test_revoked_hashed_pki_token(self): - self._test_revoked_hashed_token('signed_token_scoped') - - def test_revoked_hashed_pkiz_token(self): - self._test_revoked_hashed_token('signed_token_scoped_pkiz') - - def get_revocation_list_json(self, token_ids=None, mode=None): - if token_ids is None: - key = 'revoked_token_hash' + (('_' + mode) if mode else '') - token_ids = [self.token_dict[key]] - revocation_list = {'revoked': [{'id': x, 'expires': timeutils.utcnow()} - for x in token_ids]} - return jsonutils.dumps(revocation_list) - - def test_is_signed_token_revoked_returns_false(self): - # explicitly setting an empty revocation list here to document intent - self.middleware.token_revocation_list = jsonutils.dumps( - {"revoked": [], "extra": "success"}) - result = self.middleware.is_signed_token_revoked( - [self.token_dict['revoked_token_hash']]) - 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( - [self.token_dict['revoked_token_hash']]) - self.assertTrue(result) - - def test_is_signed_token_revoked_returns_true_sha256(self): - self.conf['hash_algorithms'] = ['sha256', 'md5'] - self.set_middleware() - self.middleware.token_revocation_list = ( - self.get_revocation_list_json(mode='sha256')) - result = self.middleware.is_signed_token_revoked( - [self.token_dict['revoked_token_hash_sha256']]) - 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, - self.token_dict['revoked_token'], - [self.token_dict['revoked_token_hash']]) - - def test_verify_signed_token_raises_exception_for_revoked_token_s256(self): - self.conf['hash_algorithms'] = ['sha256', 'md5'] - self.set_middleware() - self.middleware.token_revocation_list = ( - self.get_revocation_list_json(mode='sha256')) - self.assertRaises(auth_token.InvalidUserToken, - self.middleware.verify_signed_token, - self.token_dict['revoked_token'], - [self.token_dict['revoked_token_hash_sha256'], - self.token_dict['revoked_token_hash']]) - - def test_verify_signed_token_raises_exception_for_revoked_pkiz_token(self): - self.middleware.token_revocation_list = ( - self.examples.REVOKED_TOKEN_PKIZ_LIST_JSON) - self.assertRaises(auth_token.InvalidUserToken, - self.middleware.verify_pkiz_token, - self.token_dict['revoked_token_pkiz'], - [self.token_dict['revoked_token_pkiz_hash']]) - - def assertIsValidJSON(self, text): - json.loads(text) - - def test_verify_signed_token_succeeds_for_unrevoked_token(self): - self.middleware.token_revocation_list = self.get_revocation_list_json() - text = self.middleware.verify_signed_token( - self.token_dict['signed_token_scoped'], - [self.token_dict['signed_token_scoped_hash']]) - self.assertIsValidJSON(text) - - def test_verify_signed_compressed_token_succeeds_for_unrevoked_token(self): - self.middleware.token_revocation_list = self.get_revocation_list_json() - text = self.middleware.verify_pkiz_token( - self.token_dict['signed_token_scoped_pkiz'], - [self.token_dict['signed_token_scoped_hash']]) - self.assertIsValidJSON(text) - - def test_verify_signed_token_succeeds_for_unrevoked_token_sha256(self): - self.conf['hash_algorithms'] = ['sha256', 'md5'] - self.set_middleware() - self.middleware.token_revocation_list = ( - self.get_revocation_list_json(mode='sha256')) - text = self.middleware.verify_signed_token( - self.token_dict['signed_token_scoped'], - [self.token_dict['signed_token_scoped_hash_sha256'], - self.token_dict['signed_token_scoped_hash']]) - self.assertIsValidJSON(text) - - def test_verify_signing_dir_create_while_missing(self): - tmp_name = uuid.uuid4().hex - test_parent_signing_dir = "/tmp/%s" % tmp_name - self.middleware.signing_dirname = "/tmp/%s/%s" % ((tmp_name,) * 2) - self.middleware.signing_cert_file_name = ( - "%s/test.pem" % self.middleware.signing_dirname) - self.middleware.verify_signing_dir() - # NOTE(wu_wenxiang): Verify if the signing dir was created as expected. - self.assertTrue(os.path.isdir(self.middleware.signing_dirname)) - self.assertTrue(os.access(self.middleware.signing_dirname, os.W_OK)) - self.assertEqual(os.stat(self.middleware.signing_dirname).st_uid, - os.getuid()) - self.assertEqual( - stat.S_IMODE(os.stat(self.middleware.signing_dirname).st_mode), - stat.S_IRWXU) - shutil.rmtree(test_parent_signing_dir) - - 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.utcfromtimestamp(mtime) - self.assertEqual(fetched_time, - self.middleware.token_revocation_list_fetched_time) - - @testtools.skipUnless(TimezoneFixture.supported(), - 'TimezoneFixture not supported') - def test_get_token_revocation_list_fetched_time_returns_utc(self): - with TimezoneFixture('UTC-1'): - self.middleware.token_revocation_list = jsonutils.dumps( - self.examples.REVOCATION_LIST) - self.middleware.token_revocation_list_fetched_time = None - fetched_time = self.middleware.token_revocation_list_fetched_time - self.assertTrue(timeutils.is_soon(fetched_time, 1)) - - 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): - # auth_token uses v2 to fetch this, so don't allow the v3 - # tests to override the fake http connection - self.middleware.token_revocation_list_fetched_time = None - os.remove(self.middleware.revoked_file_name) - self.assertEqual(self.middleware.token_revocation_list, - self.examples.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): - self.requests_mock.get('%s/v2.0/tokens/revoked' % BASE_URI, text='{}') - - self.assertRaises(auth_token.ServiceError, - self.middleware.fetch_revocation_list) - - def test_fetch_revocation_list(self): - # auth_token uses v2 to fetch this, so don't allow the v3 - # tests to override the fake http connection - fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list()) - self.assertEqual(fetched_list, self.examples.REVOCATION_LIST) - - def test_request_invalid_uuid_token(self): - # remember because we are testing the middleware we stub the connection - # to the keystone server, but this is not what gets returned - invalid_uri = "%s/v2.0/tokens/invalid-token" % BASE_URI - self.requests_mock.get(invalid_uri, text="", status_code=404) - - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = 'invalid-token' - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - self.assertEqual(self.response_headers['WWW-Authenticate'], - "Keystone uri='https://keystone.example.com:1234'") - - def test_request_invalid_signed_token(self): - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = self.examples.INVALID_SIGNED_TOKEN - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(401, self.response_status) - self.assertEqual("Keystone uri='https://keystone.example.com:1234'", - self.response_headers['WWW-Authenticate']) - - def test_request_invalid_signed_pkiz_token(self): - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = self.examples.INVALID_SIGNED_PKIZ_TOKEN - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(401, self.response_status) - self.assertEqual("Keystone uri='https://keystone.example.com:1234'", - self.response_headers['WWW-Authenticate']) - - def test_request_no_token(self): - req = webob.Request.blank('/') - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - self.assertEqual(self.response_headers['WWW-Authenticate'], - "Keystone uri='https://keystone.example.com:1234'") - - def test_request_no_token_log_message(self): - class FakeLog(object): - def __init__(self): - self.msg = None - self.debugmsg = None - - def warning(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_no_token_http(self): - req = webob.Request.blank('/', environ={'REQUEST_METHOD': 'HEAD'}) - self.set_middleware() - body = self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - self.assertEqual(self.response_headers['WWW-Authenticate'], - "Keystone uri='https://keystone.example.com:1234'") - self.assertEqual(body, ['']) - - def test_request_blank_token(self): - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = '' - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - self.assertEqual(self.response_headers['WWW-Authenticate'], - "Keystone uri='https://keystone.example.com:1234'") - - def _get_cached_token(self, token, mode='md5'): - token_id = cms.cms_hash_token(token, mode=mode) - return self.middleware._token_cache._cache_get(token_id) - - def test_memcache(self): - req = webob.Request.blank('/') - token = self.token_dict['signed_token_scoped'] - req.headers['X-Auth-Token'] = token - self.middleware(req.environ, self.start_fake_response) - self.assertIsNotNone(self._get_cached_token(token)) - - def test_expired(self): - req = webob.Request.blank('/') - token = self.token_dict['signed_token_scoped_expired'] - req.headers['X-Auth-Token'] = token - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - - def test_memcache_set_invalid_uuid(self): - invalid_uri = "%s/v2.0/tokens/invalid-token" % BASE_URI - self.requests_mock.get(invalid_uri, status_code=404) - - req = webob.Request.blank('/') - token = 'invalid-token' - req.headers['X-Auth-Token'] = token - self.middleware(req.environ, self.start_fake_response) - self.assertRaises(auth_token.InvalidUserToken, - self._get_cached_token, token) - - def _test_memcache_set_invalid_signed(self, hash_algorithms=None, - exp_mode='md5'): - req = webob.Request.blank('/') - token = self.token_dict['signed_token_scoped_expired'] - req.headers['X-Auth-Token'] = token - if hash_algorithms: - self.conf['hash_algorithms'] = hash_algorithms - self.set_middleware() - self.middleware(req.environ, self.start_fake_response) - self.assertRaises(auth_token.InvalidUserToken, - self._get_cached_token, token, mode=exp_mode) - - def test_memcache_set_invalid_signed(self): - self._test_memcache_set_invalid_signed() - - def test_memcache_set_invalid_signed_sha256_md5(self): - hash_algorithms = ['sha256', 'md5'] - self._test_memcache_set_invalid_signed(hash_algorithms=hash_algorithms, - exp_mode='sha256') - - def test_memcache_set_invalid_signed_sha256(self): - hash_algorithms = ['sha256'] - self._test_memcache_set_invalid_signed(hash_algorithms=hash_algorithms, - exp_mode='sha256') - - def test_memcache_set_expired(self, extra_conf={}, extra_environ={}): - token_cache_time = 10 - conf = { - 'token_cache_time': token_cache_time, - 'signing_dir': client_fixtures.CERTDIR, - } - conf.update(extra_conf) - self.set_middleware(conf=conf) - req = webob.Request.blank('/') - token = self.token_dict['signed_token_scoped'] - req.headers['X-Auth-Token'] = token - req.environ.update(extra_environ) - - now = datetime.datetime.utcnow() - self.useFixture(TimeFixture(now)) - - self.middleware(req.environ, self.start_fake_response) - self.assertIsNotNone(self._get_cached_token(token)) - - timeutils.advance_time_seconds(token_cache_time) - self.assertIsNone(self._get_cached_token(token)) - - def test_swift_memcache_set_expired(self): - extra_conf = {'cache': 'swift.cache'} - extra_environ = {'swift.cache': memorycache.Client()} - self.test_memcache_set_expired(extra_conf, extra_environ) - - def test_http_error_not_cached_token(self): - """Test to don't cache token as invalid on network errors. - - We use UUID tokens since they are the easiest one to reach - get_http_connection. - """ - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = ERROR_TOKEN - self.middleware.http_request_max_retries = 0 - self.middleware(req.environ, self.start_fake_response) - self.assertIsNone(self._get_cached_token(ERROR_TOKEN)) - self.assert_valid_last_url(ERROR_TOKEN) - - def test_http_request_max_retries(self): - times_retry = 10 - - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = ERROR_TOKEN - - conf = {'http_request_max_retries': times_retry} - self.set_middleware(conf=conf) - - with mock.patch('time.sleep') as mock_obj: - self.middleware(req.environ, self.start_fake_response) - - self.assertEqual(mock_obj.call_count, times_retry) - - def test_nocatalog(self): - conf = { - 'include_service_catalog': False - } - self.set_middleware(conf=conf) - self.assert_valid_request_200(self.token_dict['uuid_token_default'], - with_catalog=False) - - def assert_kerberos_bind(self, token, bind_level, - use_kerberos=True, success=True): - conf = { - 'enforce_token_bind': bind_level, - 'auth_version': self.auth_version, - } - self.set_middleware(conf=conf) - - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = token - - if use_kerberos: - if use_kerberos is True: - req.environ['REMOTE_USER'] = self.examples.KERBEROS_BIND - else: - req.environ['REMOTE_USER'] = use_kerberos - - req.environ['AUTH_TYPE'] = 'Negotiate' - - body = self.middleware(req.environ, self.start_fake_response) - - if success: - self.assertEqual(self.response_status, 200) - self.assertEqual(body, [FakeApp.SUCCESS]) - self.assertIn('keystone.token_info', req.environ) - self.assert_valid_last_url(token) - else: - self.assertEqual(self.response_status, 401) - self.assertEqual(self.response_headers['WWW-Authenticate'], - "Keystone uri='https://keystone.example.com:1234'" - ) - - def test_uuid_bind_token_disabled_with_kerb_user(self): - for use_kerberos in [True, False]: - self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], - bind_level='disabled', - use_kerberos=use_kerberos, - success=True) - - def test_uuid_bind_token_disabled_with_incorrect_ticket(self): - self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], - bind_level='kerberos', - use_kerberos='ronald@MCDONALDS.COM', - success=False) - - def test_uuid_bind_token_permissive_with_kerb_user(self): - self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], - bind_level='permissive', - use_kerberos=True, - success=True) - - def test_uuid_bind_token_permissive_without_kerb_user(self): - self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], - bind_level='permissive', - use_kerberos=False, - success=False) - - def test_uuid_bind_token_permissive_with_unknown_bind(self): - token = self.token_dict['uuid_token_unknown_bind'] - - for use_kerberos in [True, False]: - self.assert_kerberos_bind(token, - bind_level='permissive', - use_kerberos=use_kerberos, - success=True) - - def test_uuid_bind_token_permissive_with_incorrect_ticket(self): - self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], - bind_level='kerberos', - use_kerberos='ronald@MCDONALDS.COM', - success=False) - - def test_uuid_bind_token_strict_with_kerb_user(self): - self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], - bind_level='strict', - use_kerberos=True, - success=True) - - def test_uuid_bind_token_strict_with_kerbout_user(self): - self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], - bind_level='strict', - use_kerberos=False, - success=False) - - def test_uuid_bind_token_strict_with_unknown_bind(self): - token = self.token_dict['uuid_token_unknown_bind'] - - for use_kerberos in [True, False]: - self.assert_kerberos_bind(token, - bind_level='strict', - use_kerberos=use_kerberos, - success=False) - - def test_uuid_bind_token_required_with_kerb_user(self): - self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], - bind_level='required', - use_kerberos=True, - success=True) - - def test_uuid_bind_token_required_without_kerb_user(self): - self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], - bind_level='required', - use_kerberos=False, - success=False) - - def test_uuid_bind_token_required_with_unknown_bind(self): - token = self.token_dict['uuid_token_unknown_bind'] - - for use_kerberos in [True, False]: - self.assert_kerberos_bind(token, - bind_level='required', - use_kerberos=use_kerberos, - success=False) - - def test_uuid_bind_token_required_without_bind(self): - for use_kerberos in [True, False]: - self.assert_kerberos_bind(self.token_dict['uuid_token_default'], - bind_level='required', - use_kerberos=use_kerberos, - success=False) - - def test_uuid_bind_token_named_kerberos_with_kerb_user(self): - self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], - bind_level='kerberos', - use_kerberos=True, - success=True) - - def test_uuid_bind_token_named_kerberos_without_kerb_user(self): - self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], - bind_level='kerberos', - use_kerberos=False, - success=False) - - def test_uuid_bind_token_named_kerberos_with_unknown_bind(self): - token = self.token_dict['uuid_token_unknown_bind'] - - for use_kerberos in [True, False]: - self.assert_kerberos_bind(token, - bind_level='kerberos', - use_kerberos=use_kerberos, - success=False) - - def test_uuid_bind_token_named_kerberos_without_bind(self): - for use_kerberos in [True, False]: - self.assert_kerberos_bind(self.token_dict['uuid_token_default'], - bind_level='kerberos', - use_kerberos=use_kerberos, - success=False) - - def test_uuid_bind_token_named_kerberos_with_incorrect_ticket(self): - self.assert_kerberos_bind(self.token_dict['uuid_token_bind'], - bind_level='kerberos', - use_kerberos='ronald@MCDONALDS.COM', - success=False) - - def test_uuid_bind_token_with_unknown_named_FOO(self): - token = self.token_dict['uuid_token_bind'] - - for use_kerberos in [True, False]: - self.assert_kerberos_bind(token, - bind_level='FOO', - use_kerberos=use_kerberos, - success=False) - - -class V2CertDownloadMiddlewareTest(BaseAuthTokenMiddlewareTest, - testresources.ResourcedTestCase): - - resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] - - def __init__(self, *args, **kwargs): - super(V2CertDownloadMiddlewareTest, self).__init__(*args, **kwargs) - self.auth_version = 'v2.0' - self.fake_app = None - self.ca_path = '/v2.0/certificates/ca' - self.signing_path = '/v2.0/certificates/signing' - - def setUp(self): - super(V2CertDownloadMiddlewareTest, self).setUp( - auth_version=self.auth_version, - fake_app=self.fake_app) - self.base_dir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, self.base_dir) - self.cert_dir = os.path.join(self.base_dir, 'certs') - os.makedirs(self.cert_dir, stat.S_IRWXU) - conf = { - 'signing_dir': self.cert_dir, - 'auth_version': self.auth_version, - } - self.set_middleware(conf=conf) - - # Usually we supply a signed_dir with pre-installed certificates, - # so invocation of /usr/bin/openssl succeeds. This time we give it - # an empty directory, so it fails. - def test_request_no_token_dummy(self): - cms._ensure_subprocess() - - self.requests_mock.get("%s%s" % (BASE_URI, self.ca_path), - status_code=404) - url = "%s%s" % (BASE_URI, self.signing_path) - self.requests_mock.get(url, status_code=404) - self.assertRaises(exceptions.CertificateConfigError, - self.middleware.verify_signed_token, - self.examples.SIGNED_TOKEN_SCOPED, - [self.examples.SIGNED_TOKEN_SCOPED_HASH]) - - def test_fetch_signing_cert(self): - data = 'FAKE CERT' - url = '%s%s' % (BASE_URI, self.signing_path) - self.requests_mock.get(url, text=data) - self.middleware.fetch_signing_cert() - - with open(self.middleware.signing_cert_file_name, 'r') as f: - self.assertEqual(f.read(), data) - - self.assertLastPath("/testadmin%s" % self.signing_path) - - def test_fetch_signing_ca(self): - data = 'FAKE CA' - self.requests_mock.get("%s%s" % (BASE_URI, self.ca_path), text=data) - self.middleware.fetch_ca_cert() - - with open(self.middleware.signing_ca_file_name, 'r') as f: - self.assertEqual(f.read(), data) - - self.assertLastPath("/testadmin%s" % self.ca_path) - - def test_prefix_trailing_slash(self): - del self.conf['identity_uri'] - self.conf['auth_protocol'] = 'https' - self.conf['auth_host'] = 'keystone.example.com' - self.conf['auth_port'] = 1234 - self.conf['auth_admin_prefix'] = '/newadmin/' - - self.requests_mock.get("%s/newadmin%s" % (BASE_HOST, self.ca_path), - text='FAKECA') - url = "%s/newadmin%s" % (BASE_HOST, self.signing_path) - self.requests_mock.get(url, text='FAKECERT') - - self.set_middleware(conf=self.conf) - - self.middleware.fetch_ca_cert() - - self.assertLastPath('/newadmin%s' % self.ca_path) - - self.middleware.fetch_signing_cert() - - self.assertLastPath('/newadmin%s' % self.signing_path) - - def test_without_prefix(self): - del self.conf['identity_uri'] - self.conf['auth_protocol'] = 'https' - self.conf['auth_host'] = 'keystone.example.com' - self.conf['auth_port'] = 1234 - self.conf['auth_admin_prefix'] = '' - - self.requests_mock.get("%s%s" % (BASE_HOST, self.ca_path), - text='FAKECA') - self.requests_mock.get("%s%s" % (BASE_HOST, self.signing_path), - text='FAKECERT') - - self.set_middleware(conf=self.conf) - - self.middleware.fetch_ca_cert() - - self.assertLastPath(self.ca_path) - - self.middleware.fetch_signing_cert() - - self.assertLastPath(self.signing_path) - - -class V3CertDownloadMiddlewareTest(V2CertDownloadMiddlewareTest): - - def __init__(self, *args, **kwargs): - super(V3CertDownloadMiddlewareTest, self).__init__(*args, **kwargs) - self.auth_version = 'v3.0' - self.fake_app = v3FakeApp - self.ca_path = '/v3/OS-SIMPLE-CERT/ca' - self.signing_path = '/v3/OS-SIMPLE-CERT/certificates' - - -def network_error_response(method, uri, headers): - raise auth_token.NetworkError("Network connection error.") - - -class v2AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, - CommonAuthTokenMiddlewareTest, - testresources.ResourcedTestCase): - """v2 token specific tests. - - There are some differences between how the auth-token middleware handles - v2 and v3 tokens over and above the token formats, namely: - - - A v3 keystone server will auto scope a token to a user's default project - if no scope is specified. A v2 server assumes that the auth-token - middleware will do that. - - A v2 keystone server may issue a token without a catalog, even with a - tenant - - The tests below were originally part of the generic AuthTokenMiddlewareTest - class, but now, since they really are v2 specific, they are included here. - - """ - - resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] - - def setUp(self): - super(v2AuthTokenMiddlewareTest, self).setUp() - - self.token_dict = { - 'uuid_token_default': self.examples.UUID_TOKEN_DEFAULT, - 'uuid_token_unscoped': self.examples.UUID_TOKEN_UNSCOPED, - 'uuid_token_bind': self.examples.UUID_TOKEN_BIND, - 'uuid_token_unknown_bind': self.examples.UUID_TOKEN_UNKNOWN_BIND, - 'signed_token_scoped': self.examples.SIGNED_TOKEN_SCOPED, - 'signed_token_scoped_pkiz': self.examples.SIGNED_TOKEN_SCOPED_PKIZ, - 'signed_token_scoped_hash': self.examples.SIGNED_TOKEN_SCOPED_HASH, - 'signed_token_scoped_hash_sha256': - self.examples.SIGNED_TOKEN_SCOPED_HASH_SHA256, - 'signed_token_scoped_expired': - self.examples.SIGNED_TOKEN_SCOPED_EXPIRED, - 'revoked_token': self.examples.REVOKED_TOKEN, - 'revoked_token_pkiz': self.examples.REVOKED_TOKEN_PKIZ, - 'revoked_token_pkiz_hash': - self.examples.REVOKED_TOKEN_PKIZ_HASH, - 'revoked_token_hash': self.examples.REVOKED_TOKEN_HASH, - 'revoked_token_hash_sha256': - self.examples.REVOKED_TOKEN_HASH_SHA256, - } - - self.requests_mock.get("%s/" % BASE_URI, - text=VERSION_LIST_v2, - status_code=300) - - self.requests_mock.post("%s/v2.0/tokens" % BASE_URI, - text=FAKE_ADMIN_TOKEN) - - self.requests_mock.get("%s/v2.0/tokens/revoked" % BASE_URI, - text=self.examples.SIGNED_REVOCATION_LIST) - - for token in (self.examples.UUID_TOKEN_DEFAULT, - self.examples.UUID_TOKEN_UNSCOPED, - self.examples.UUID_TOKEN_BIND, - self.examples.UUID_TOKEN_UNKNOWN_BIND, - self.examples.UUID_TOKEN_NO_SERVICE_CATALOG, - self.examples.SIGNED_TOKEN_SCOPED_KEY, - self.examples.SIGNED_TOKEN_SCOPED_PKIZ_KEY,): - text = self.examples.JSON_TOKEN_RESPONSES[token] - self.requests_mock.get('%s/v2.0/tokens/%s' % (BASE_URI, token), - text=text) - - self.requests_mock.get('%s/v2.0/tokens/%s' % (BASE_URI, ERROR_TOKEN), - text=network_error_response) - - self.set_middleware() - - def assert_unscoped_default_tenant_auto_scopes(self, token): - """Unscoped v2 requests with a default tenant should "auto-scope." - - The implied scope is the user's tenant ID. - - """ - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = token - body = self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 200) - self.assertEqual(body, [FakeApp.SUCCESS]) - self.assertIn('keystone.token_info', req.environ) - - def assert_valid_last_url(self, token_id): - self.assertLastPath("/testadmin/v2.0/tokens/%s" % token_id) - - def test_default_tenant_uuid_token(self): - self.assert_unscoped_default_tenant_auto_scopes( - self.examples.UUID_TOKEN_DEFAULT) - - def test_default_tenant_signed_token(self): - self.assert_unscoped_default_tenant_auto_scopes( - self.examples.SIGNED_TOKEN_SCOPED) - - def assert_unscoped_token_receives_401(self, token): - """Unscoped requests with no default tenant ID should be rejected.""" - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = token - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 401) - self.assertEqual(self.response_headers['WWW-Authenticate'], - "Keystone uri='https://keystone.example.com:1234'") - - def test_unscoped_uuid_token_receives_401(self): - self.assert_unscoped_token_receives_401( - self.examples.UUID_TOKEN_UNSCOPED) - - def test_unscoped_pki_token_receives_401(self): - self.assert_unscoped_token_receives_401( - self.examples.SIGNED_TOKEN_UNSCOPED) - - def test_request_prevent_service_catalog_injection(self): - req = webob.Request.blank('/') - req.headers['X-Service-Catalog'] = '[]' - req.headers['X-Auth-Token'] = ( - self.examples.UUID_TOKEN_NO_SERVICE_CATALOG) - body = self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 200) - self.assertFalse(req.headers.get('X-Service-Catalog')) - self.assertEqual(body, [FakeApp.SUCCESS]) - - -class CrossVersionAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, - testresources.ResourcedTestCase): - - resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] - - def test_valid_uuid_request_forced_to_2_0(self): - """Test forcing auth_token to use lower api version. - - By installing the v3 http hander, auth_token will be get - a version list that looks like a v3 server - from which it - would normally chose v3.0 as the auth version. However, here - we specify v2.0 in the configuration - which should force - auth_token to use that version instead. - - """ - conf = { - 'signing_dir': client_fixtures.CERTDIR, - 'auth_version': 'v2.0' - } - - self.requests_mock.get('%s/' % BASE_URI, - text=VERSION_LIST_v3, - status_code=300) - - self.requests_mock.post('%s/v2.0/tokens' % BASE_URI, - text=FAKE_ADMIN_TOKEN) - - token = self.examples.UUID_TOKEN_DEFAULT - url = '%s/v2.0/tokens/%s' % (BASE_URI, token) - response_body = self.examples.JSON_TOKEN_RESPONSES[token] - self.requests_mock.get(url, text=response_body) - - self.set_middleware(conf=conf) - - # This tests will only work is auth_token has chosen to use the - # lower, v2, api version - req = webob.Request.blank('/') - req.headers['X-Auth-Token'] = self.examples.UUID_TOKEN_DEFAULT - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 200) - self.assertLastPath("/testadmin/v2.0/tokens/%s" % - self.examples.UUID_TOKEN_DEFAULT) - - -class v3AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, - CommonAuthTokenMiddlewareTest, - testresources.ResourcedTestCase): - """Test auth_token middleware with v3 tokens. - - Re-execute the AuthTokenMiddlewareTest class tests, but with the - auth_token middleware configured to expect v3 tokens back from - a keystone server. - - This is done by configuring the AuthTokenMiddlewareTest class via - its Setup(), passing in v3 style data that will then be used by - the tests themselves. This approach has been used to ensure we - really are running the same tests for both v2 and v3 tokens. - - There a few additional specific test for v3 only: - - - We allow an unscoped token to be validated (as unscoped), where - as for v2 tokens, the auth_token middleware is expected to try and - auto-scope it (and fail if there is no default tenant) - - Domain scoped tokens - - Since we don't specify an auth version for auth_token to use, by - definition we are thefore implicitely testing that it will use - the highest available auth version, i.e. v3.0 - - """ - - resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] - - def setUp(self): - super(v3AuthTokenMiddlewareTest, self).setUp( - auth_version='v3.0', - fake_app=v3FakeApp) - - self.token_dict = { - 'uuid_token_default': self.examples.v3_UUID_TOKEN_DEFAULT, - 'uuid_token_unscoped': self.examples.v3_UUID_TOKEN_UNSCOPED, - 'uuid_token_bind': self.examples.v3_UUID_TOKEN_BIND, - 'uuid_token_unknown_bind': - self.examples.v3_UUID_TOKEN_UNKNOWN_BIND, - 'signed_token_scoped': self.examples.SIGNED_v3_TOKEN_SCOPED, - 'signed_token_scoped_pkiz': - self.examples.SIGNED_v3_TOKEN_SCOPED_PKIZ, - 'signed_token_scoped_hash': - self.examples.SIGNED_v3_TOKEN_SCOPED_HASH, - 'signed_token_scoped_hash_sha256': - self.examples.SIGNED_v3_TOKEN_SCOPED_HASH_SHA256, - 'signed_token_scoped_expired': - self.examples.SIGNED_TOKEN_SCOPED_EXPIRED, - 'revoked_token': self.examples.REVOKED_v3_TOKEN, - 'revoked_token_pkiz': self.examples.REVOKED_v3_TOKEN_PKIZ, - 'revoked_token_hash': self.examples.REVOKED_v3_TOKEN_HASH, - 'revoked_token_hash_sha256': - self.examples.REVOKED_v3_TOKEN_HASH_SHA256, - 'revoked_token_pkiz_hash': - self.examples.REVOKED_v3_PKIZ_TOKEN_HASH, - } - - self.requests_mock.get(BASE_URI, text=VERSION_LIST_v3, status_code=300) - - # TODO(jamielennox): auth_token middleware uses a v2 admin token - # regardless of the auth_version that is set. - self.requests_mock.post('%s/v2.0/tokens' % BASE_URI, - text=FAKE_ADMIN_TOKEN) - - # TODO(jamielennox): there is no v3 revocation url yet, it uses v2 - self.requests_mock.get('%s/v2.0/tokens/revoked' % BASE_URI, - text=self.examples.SIGNED_REVOCATION_LIST) - - self.requests_mock.get('%s/v3/auth/tokens' % BASE_URI, - text=self.token_response) - - self.set_middleware() - - def token_response(self, request, context): - auth_id = request.headers.get('X-Auth-Token') - token_id = request.headers.get('X-Subject-Token') - self.assertEqual(auth_id, FAKE_ADMIN_TOKEN_ID) - - response = "" - - if token_id == ERROR_TOKEN: - raise auth_token.NetworkError("Network connection error.") - - try: - response = self.examples.JSON_TOKEN_RESPONSES[token_id] - except KeyError: - context.status_code = 404 - - return response - - def assert_valid_last_url(self, token_id): - self.assertLastPath('/testadmin/v3/auth/tokens') - - def test_valid_unscoped_uuid_request(self): - # Remove items that won't be in an unscoped token - delta_expected_env = { - 'HTTP_X_PROJECT_ID': None, - 'HTTP_X_PROJECT_NAME': None, - 'HTTP_X_PROJECT_DOMAIN_ID': None, - 'HTTP_X_PROJECT_DOMAIN_NAME': None, - 'HTTP_X_TENANT_ID': None, - 'HTTP_X_TENANT_NAME': None, - 'HTTP_X_ROLES': '', - 'HTTP_X_TENANT': None, - 'HTTP_X_ROLE': '', - } - self.set_middleware(expected_env=delta_expected_env) - self.assert_valid_request_200(self.examples.v3_UUID_TOKEN_UNSCOPED, - with_catalog=False) - self.assertLastPath('/testadmin/v3/auth/tokens') - - def test_domain_scoped_uuid_request(self): - # Modify items compared to default token for a domain scope - delta_expected_env = { - 'HTTP_X_DOMAIN_ID': 'domain_id1', - 'HTTP_X_DOMAIN_NAME': 'domain_name1', - 'HTTP_X_PROJECT_ID': None, - 'HTTP_X_PROJECT_NAME': None, - 'HTTP_X_PROJECT_DOMAIN_ID': None, - 'HTTP_X_PROJECT_DOMAIN_NAME': None, - 'HTTP_X_TENANT_ID': None, - 'HTTP_X_TENANT_NAME': None, - 'HTTP_X_TENANT': None - } - self.set_middleware(expected_env=delta_expected_env) - self.assert_valid_request_200( - self.examples.v3_UUID_TOKEN_DOMAIN_SCOPED) - self.assertLastPath('/testadmin/v3/auth/tokens') - - def test_gives_v2_catalog(self): - self.set_middleware() - req = self.assert_valid_request_200( - self.examples.SIGNED_v3_TOKEN_SCOPED) - - catalog = jsonutils.loads(req.headers['X-Service-Catalog']) - - for service in catalog: - for endpoint in service['endpoints']: - # no point checking everything, just that it's in v2 format - self.assertIn('adminURL', endpoint) - self.assertIn('publicURL', endpoint) - self.assertIn('adminURL', endpoint) - - -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')) - - -class TokenExpirationTest(BaseAuthTokenMiddlewareTest): - def setUp(self): - super(TokenExpirationTest, self).setUp() - self.now = timeutils.utcnow() - self.delta = datetime.timedelta(hours=1) - self.one_hour_ago = timeutils.isotime(self.now - self.delta, - subsecond=True) - self.one_hour_earlier = timeutils.isotime(self.now + self.delta, - subsecond=True) - - def create_v2_token_fixture(self, expires=None): - v2_fixture = { - 'access': { - 'token': { - 'id': 'blah', - 'expires': expires or self.one_hour_earlier, - 'tenant': { - 'id': 'tenant_id1', - 'name': 'tenant_name1', - }, - }, - 'user': { - 'id': 'user_id1', - 'name': 'user_name1', - 'roles': [ - {'name': 'role1'}, - {'name': 'role2'}, - ], - }, - 'serviceCatalog': {} - }, - } - - return v2_fixture - - def create_v3_token_fixture(self, expires=None): - - v3_fixture = { - 'token': { - 'expires_at': expires or self.one_hour_earlier, - 'user': { - 'id': 'user_id1', - 'name': 'user_name1', - 'domain': { - 'id': 'domain_id1', - 'name': 'domain_name1' - } - }, - 'project': { - 'id': 'tenant_id1', - 'name': 'tenant_name1', - 'domain': { - 'id': 'domain_id1', - 'name': 'domain_name1' - } - }, - 'roles': [ - {'name': 'role1', 'id': 'Role1'}, - {'name': 'role2', 'id': 'Role2'}, - ], - 'catalog': {} - } - } - - return v3_fixture - - def test_no_data(self): - data = {} - self.assertRaises(auth_token.InvalidUserToken, - auth_token.confirm_token_not_expired, - data) - - def test_bad_data(self): - data = {'my_happy_token_dict': 'woo'} - self.assertRaises(auth_token.InvalidUserToken, - auth_token.confirm_token_not_expired, - data) - - def test_v2_token_not_expired(self): - data = self.create_v2_token_fixture() - expected_expires = data['access']['token']['expires'] - actual_expires = auth_token.confirm_token_not_expired(data) - self.assertEqual(actual_expires, expected_expires) - - def test_v2_token_expired(self): - data = self.create_v2_token_fixture(expires=self.one_hour_ago) - self.assertRaises(auth_token.InvalidUserToken, - auth_token.confirm_token_not_expired, - data) - - def test_v2_token_with_timezone_offset_not_expired(self): - self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z')) - data = self.create_v2_token_fixture( - expires='2000-01-01T00:05:10.000123-05:00') - expected_expires = '2000-01-01T05:05:10.000123Z' - actual_expires = auth_token.confirm_token_not_expired(data) - self.assertEqual(actual_expires, expected_expires) - - def test_v2_token_with_timezone_offset_expired(self): - self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z')) - data = self.create_v2_token_fixture( - expires='2000-01-01T00:05:10.000123+05:00') - data['access']['token']['expires'] = '2000-01-01T00:05:10.000123+05:00' - self.assertRaises(auth_token.InvalidUserToken, - auth_token.confirm_token_not_expired, - data) - - def test_v3_token_not_expired(self): - data = self.create_v3_token_fixture() - expected_expires = data['token']['expires_at'] - actual_expires = auth_token.confirm_token_not_expired(data) - self.assertEqual(actual_expires, expected_expires) - - def test_v3_token_expired(self): - data = self.create_v3_token_fixture(expires=self.one_hour_ago) - self.assertRaises(auth_token.InvalidUserToken, - auth_token.confirm_token_not_expired, - data) - - def test_v3_token_with_timezone_offset_not_expired(self): - self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z')) - data = self.create_v3_token_fixture( - expires='2000-01-01T00:05:10.000123-05:00') - expected_expires = '2000-01-01T05:05:10.000123Z' - - actual_expires = auth_token.confirm_token_not_expired(data) - self.assertEqual(actual_expires, expected_expires) - - def test_v3_token_with_timezone_offset_expired(self): - self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z')) - data = self.create_v3_token_fixture( - expires='2000-01-01T00:05:10.000123+05:00') - self.assertRaises(auth_token.InvalidUserToken, - auth_token.confirm_token_not_expired, - data) - - def test_cached_token_not_expired(self): - token = 'mytoken' - data = 'this_data' - self.set_middleware() - self.middleware._token_cache.initialize({}) - some_time_later = timeutils.strtime(at=(self.now + self.delta)) - expires = some_time_later - self.middleware._token_cache.store(token, data, expires) - self.assertEqual(self.middleware._token_cache._cache_get(token), data) - - def test_cached_token_not_expired_with_old_style_nix_timestamp(self): - """Ensure we cannot retrieve a token from the cache. - - Getting a token from the cache should return None when the token data - in the cache stores the expires time as a \*nix style timestamp. - - """ - token = 'mytoken' - data = 'this_data' - self.set_middleware() - token_cache = self.middleware._token_cache - token_cache.initialize({}) - some_time_later = self.now + self.delta - # Store a unix timestamp in the cache. - expires = calendar.timegm(some_time_later.timetuple()) - token_cache.store(token, data, expires) - self.assertIsNone(token_cache._cache_get(token)) - - def test_cached_token_expired(self): - token = 'mytoken' - data = 'this_data' - self.set_middleware() - self.middleware._token_cache.initialize({}) - some_time_earlier = timeutils.strtime(at=(self.now - self.delta)) - expires = some_time_earlier - self.middleware._token_cache.store(token, data, expires) - self.assertThat(lambda: self.middleware._token_cache._cache_get(token), - matchers.raises(auth_token.InvalidUserToken)) - - def test_cached_token_with_timezone_offset_not_expired(self): - token = 'mytoken' - data = 'this_data' - self.set_middleware() - self.middleware._token_cache.initialize({}) - timezone_offset = datetime.timedelta(hours=2) - some_time_later = self.now - timezone_offset + self.delta - expires = timeutils.strtime(some_time_later) + '-02:00' - self.middleware._token_cache.store(token, data, expires) - self.assertEqual(self.middleware._token_cache._cache_get(token), data) - - def test_cached_token_with_timezone_offset_expired(self): - token = 'mytoken' - data = 'this_data' - self.set_middleware() - self.middleware._token_cache.initialize({}) - timezone_offset = datetime.timedelta(hours=2) - some_time_earlier = self.now - timezone_offset - self.delta - expires = timeutils.strtime(some_time_earlier) + '-02:00' - self.middleware._token_cache.store(token, data, expires) - self.assertThat(lambda: self.middleware._token_cache._cache_get(token), - matchers.raises(auth_token.InvalidUserToken)) - - -class CatalogConversionTests(BaseAuthTokenMiddlewareTest): - - PUBLIC_URL = 'http://server:5000/v2.0' - ADMIN_URL = 'http://admin:35357/v2.0' - INTERNAL_URL = 'http://internal:5000/v2.0' - - REGION_ONE = 'RegionOne' - REGION_TWO = 'RegionTwo' - REGION_THREE = 'RegionThree' - - def test_basic_convert(self): - token = fixture.V3Token() - s = token.add_service(type='identity') - s.add_standard_endpoints(public=self.PUBLIC_URL, - admin=self.ADMIN_URL, - internal=self.INTERNAL_URL, - region=self.REGION_ONE) - - auth_ref = access.AccessInfo.factory(body=token) - catalog_data = auth_ref.service_catalog.get_data() - catalog = auth_token._v3_to_v2_catalog(catalog_data) - - self.assertEqual(1, len(catalog)) - service = catalog[0] - self.assertEqual(1, len(service['endpoints'])) - endpoints = service['endpoints'][0] - - self.assertEqual('identity', service['type']) - self.assertEqual(4, len(endpoints)) - self.assertEqual(self.PUBLIC_URL, endpoints['publicURL']) - self.assertEqual(self.ADMIN_URL, endpoints['adminURL']) - self.assertEqual(self.INTERNAL_URL, endpoints['internalURL']) - self.assertEqual(self.REGION_ONE, endpoints['region']) - - def test_multi_region(self): - token = fixture.V3Token() - s = token.add_service(type='identity') - - s.add_endpoint('internal', self.INTERNAL_URL, region=self.REGION_ONE) - s.add_endpoint('public', self.PUBLIC_URL, region=self.REGION_TWO) - s.add_endpoint('admin', self.ADMIN_URL, region=self.REGION_THREE) - - auth_ref = access.AccessInfo.factory(body=token) - catalog_data = auth_ref.service_catalog.get_data() - catalog = auth_token._v3_to_v2_catalog(catalog_data) - - self.assertEqual(1, len(catalog)) - service = catalog[0] - - # the 3 regions will come through as 3 separate endpoints - expected = [{'internalURL': self.INTERNAL_URL, - 'region': self.REGION_ONE}, - {'publicURL': self.PUBLIC_URL, - 'region': self.REGION_TWO}, - {'adminURL': self.ADMIN_URL, - 'region': self.REGION_THREE}] - - self.assertEqual('identity', service['type']) - self.assertEqual(3, len(service['endpoints'])) - for e in expected: - self.assertIn(e, expected) - - -def load_tests(loader, tests, pattern): - return testresources.OptimisingTestSuite(tests) diff --git a/keystoneclient/tests/unit/test_base.py b/keystoneclient/tests/unit/test_base.py index 2e7fc5e28..ca57dc469 100644 --- a/keystoneclient/tests/unit/test_base.py +++ b/keystoneclient/tests/unit/test_base.py @@ -1,3 +1,4 @@ +# -*- 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 @@ -10,10 +11,29 @@ # 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): @@ -34,15 +54,14 @@ class TmpObject(object): self.assertEqual(base.getid(TmpObject), 4) def test_resource_lazy_getattr(self): - self.client = client.Client(username=self.TEST_USER, - token=self.TEST_TOKEN, - tenant_name=self.TEST_TENANT_NAME, - auth_url='http://127.0.0.1:5000', - endpoint='http://127.0.0.1:5000') + 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.client._adapter.get = self.mox.CreateMockAnything() - self.client._adapter.get('/OS-KSADM/roles/1').AndRaise(AttributeError) - self.mox.ReplayAll() + 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') @@ -51,24 +70,41 @@ def test_resource_lazy_getattr(self): self.assertRaises(AttributeError, getattr, f, 'blahblah') def test_eq(self): - # Two resources of the same type with the same id: equal + # 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.assertEqual(r1, r2) + 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 resoruces of different types: never equal + # 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.assertEqual(r1, r2) + 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 !"}) @@ -76,6 +112,15 @@ def test_human_id(self): 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}} @@ -83,79 +128,298 @@ class ManagerTest(utils.TestCase): def setUp(self): super(ManagerTest, self).setUp() - self.client = client.Client(username=self.TEST_USER, - token=self.TEST_TOKEN, - tenant_name=self.TEST_TENANT_NAME, - auth_url='http://127.0.0.1:5000', - endpoint='http://127.0.0.1:5000') + + 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): - self.assertEqual(self.mgr.api, self.client) + with self.deprecations.expect_deprecations_here(): + self.assertEqual(self.mgr.api, self.client) def test_get(self): - self.client.get = self.mox.CreateMockAnything() - self.client.get(self.url).AndReturn((None, self.body)) - self.mox.ReplayAll() - + 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): - self.client.post = self.mox.CreateMockAnything() - self.client.post(self.url, body=self.body).AndReturn((None, self.body)) - self.client.post(self.url, body=self.body).AndReturn((None, self.body)) - self.mox.ReplayAll() + 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): - self.client.put = self.mox.CreateMockAnything() - self.client.put(self.url, body=self.body).AndReturn((None, self.body)) - self.client.put(self.url, body=self.body).AndReturn((None, self.body)) - self.mox.ReplayAll() + 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): - self.client.patch = self.mox.CreateMockAnything() - self.client.patch(self.url, body=self.body).AndReturn( - (None, self.body)) - self.client.patch(self.url, body=self.body).AndReturn( - (None, self.body)) - self.mox.ReplayAll() + 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): - self.client.patch = self.mox.CreateMockAnything() - self.client.put = self.mox.CreateMockAnything() - self.client.patch( - self.url, body=self.body, management=False).AndReturn((None, - self.body)) - self.client.put(self.url, body=None, management=True).AndReturn( - (None, self.body)) - self.mox.ReplayAll() + 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 index 019730d6b..2671bcb49 100644 --- a/keystoneclient/tests/unit/test_cms.py +++ b/keystoneclient/tests/unit/test_cms.py @@ -13,8 +13,8 @@ import errno import os import subprocess +from unittest import mock -import mock import testresources from testtools import matchers @@ -25,7 +25,6 @@ class CMSTest(utils.TestCase, testresources.ResourcedTestCase): - """Unit tests for the keystoneclient.common.cms module.""" resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] @@ -42,6 +41,11 @@ def __init__(self, *args, **kwargs): 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, @@ -90,12 +94,8 @@ def test_cms_verify_token_no_files(self): '/no/such/file', '/no/such/key') def test_cms_verify_token_no_oserror(self): - def raise_OSError(*args): - e = OSError() - e.errno = errno.EPIPE - raise e - - with mock.patch('subprocess.Popen.communicate', new=raise_OSError): + 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: @@ -155,6 +155,47 @@ def test_cms_hash_token_sha256(self): # 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 index 76aaf0377..57c7002cc 100644 --- a/keystoneclient/tests/unit/test_discovery.py +++ b/keystoneclient/tests/unit/test_discovery.py @@ -13,8 +13,8 @@ import re import uuid +from keystoneauth1 import fixture from oslo_serialization import jsonutils -import six from testtools import matchers from keystoneclient import _discover @@ -22,7 +22,6 @@ from keystoneclient import client from keystoneclient import discover from keystoneclient import exceptions -from keystoneclient import fixture from keystoneclient import session from keystoneclient.tests.unit import utils from keystoneclient.v2_0 import client as v2_client @@ -87,7 +86,7 @@ V2_AUTH_RESPONSE = jsonutils.dumps({ "access": { "token": { - "expires": "2020-01-01T00:00:10.000123Z", + "expires": "2999-01-01T00:00:10.000123Z", "id": 'fakeToken', "tenant": { "id": '1' @@ -105,7 +104,7 @@ V3_MEDIA_TYPES = V3_VERSION.media_types V3_VERSION.updated_str = UPDATED -V3_TOKEN = six.u('3e2813b7ba0b4006840c3825860b86ed'), +V3_TOKEN = ('3e2813b7ba0b4006840c3825860b86ed',) V3_AUTH_RESPONSE = jsonutils.dumps({ "token": { "methods": [ @@ -113,7 +112,7 @@ "password" ], - "expires_at": "2020-01-01T00:00:10.000123Z", + "expires_at": "2999-01-01T00:00:10.000123Z", "project": { "domain": { "id": '1', @@ -234,12 +233,16 @@ def _create_single_version(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 six.iteritems(examples): + for path, text in examples.items(): url = "%s%s" % (BASE_URL, path) self.requests_mock.get(url, status_code=300, text=text) @@ -310,6 +313,10 @@ def test_available_glance_data(self): 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, @@ -472,7 +479,8 @@ def test_pass_client_arguments(self): cl = self.assertCreatesV2(auth_url=BASE_URL, **kwargs) - self.assertEqual(cl.original_ip, '100') + with self.deprecations.expect_deprecations_here(): + self.assertEqual(cl.original_ip, '100') self.assertEqual(cl.stale_duration, 15) self.assertFalse(cl.use_keyring) @@ -483,12 +491,13 @@ def test_overriding_stored_kwargs(self): text=V3_AUTH_RESPONSE, headers={'X-Subject-Token': V3_TOKEN}) - disc = discover.Discover(auth_url=BASE_URL, debug=False, - username='foo') + # 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.assertTrue(client.debug_log) self.assertFalse(disc._client_kwargs['debug']) self.assertEqual(client.username, 'foo') self.assertEqual(client.password, 'bar') @@ -497,9 +506,12 @@ def test_available_versions(self): self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_ENTRY) - disc = discover.Discover(auth_url=BASE_URL) + # Creating Discover not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + disc = discover.Discover(auth_url=BASE_URL) - versions = disc.available_versions() + with self.deprecations.expect_deprecations_here(): + versions = disc.available_versions() self.assertEqual(1, len(versions)) self.assertEqual(V3_VERSION, versions[0]) @@ -513,7 +525,9 @@ def test_unknown_client_version(self): versions.add_version(V4_VERSION) self.requests_mock.get(BASE_URL, status_code=300, json=versions) - disc = discover.Discover(auth_url=BASE_URL) + # 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) @@ -521,7 +535,9 @@ 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) - disc = discover.Discover(auth_url=BASE_URL) + # 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)) @@ -532,8 +548,10 @@ def _do_discovery_call(self, token=None, **kwargs): token = uuid.uuid4().hex url = 'http://testurl' - a = token_endpoint.Token(url, token) - s = session.Session(auth=a) + + 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) @@ -556,7 +574,9 @@ class DiscoverQueryTests(utils.TestCase): def test_available_keystone_data(self): self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST) - disc = discover.Discover(auth_url=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((2, 0), versions[0]['version']) @@ -587,7 +607,9 @@ def test_available_cinder_data(self): v1_url = "%sv1/" % BASE_URL v2_url = "%sv2/" % BASE_URL - disc = discover.Discover(auth_url=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']) @@ -618,7 +640,9 @@ def test_available_glance_data(self): v1_url = "%sv1/" % BASE_URL v2_url = "%sv2/" % BASE_URL - disc = discover.Discover(auth_url=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']) @@ -664,7 +688,9 @@ def test_allow_deprecated(self): text = jsonutils.dumps({'versions': version_list}) self.requests_mock.get(BASE_URL, text=text) - disc = discover.Discover(auth_url=BASE_URL) + # 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) @@ -686,7 +712,9 @@ def test_allow_experimental(self): text = jsonutils.dumps({'versions': version_list}) self.requests_mock.get(BASE_URL, text=text) - disc = discover.Discover(auth_url=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(0, len(versions)) @@ -702,7 +730,9 @@ def test_allow_unknown(self): version_list = fixture.DiscoveryList(BASE_URL, v2=False, v3_status=status) self.requests_mock.get(BASE_URL, json=version_list) - disc = discover.Discover(auth_url=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(0, len(versions)) @@ -732,7 +762,9 @@ def test_ignoring_invalid_lnks(self): text = jsonutils.dumps({'versions': version_list}) self.requests_mock.get(BASE_URL, text=text) - disc = discover.Discover(auth_url=BASE_URL) + # 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() diff --git a/keystoneclient/tests/unit/test_ec2utils.py b/keystoneclient/tests/unit/test_ec2utils.py index 71fc176b5..12cf57531 100644 --- a/keystoneclient/tests/unit/test_ec2utils.py +++ b/keystoneclient/tests/unit/test_ec2utils.py @@ -12,17 +12,17 @@ # License for the specific language governing permissions and limitations # under the License. -from __future__ import unicode_literals - 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) @@ -187,9 +187,10 @@ def test_generate_v4_port(self): self.assertEqual(signature, expected) def test_generate_v4_port_strip(self): - """Test v4 generator with host:port format, but for an old - (<2.9.3) version of boto, where the port should be stripped - to match boto behavior. + """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' @@ -225,8 +226,10 @@ def test_generate_v4_port_strip(self): self.assertEqual(expected, signature) def test_generate_v4_port_nostrip(self): - """Test v4 generator with host:port format, but for an new - (>=2.9.3) version of boto, where the port should not be stripped. + """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' @@ -260,3 +263,42 @@ def test_generate_v4_port_nostrip(self): 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 index 345ae457c..d7fd26e66 100644 --- a/keystoneclient/tests/unit/test_fixtures.py +++ b/keystoneclient/tests/unit/test_fixtures.py @@ -12,7 +12,6 @@ import uuid -import six from keystoneclient import fixture from keystoneclient.tests.unit import utils @@ -246,7 +245,7 @@ def test_catalog(self): # the endpoint content below easier. self.assertTrue(endpoint.pop('id')) - for interface, url in six.iteritems(endpoints): + 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_hacking_checks.py b/keystoneclient/tests/unit/test_hacking_checks.py deleted file mode 100644 index 220d258cc..000000000 --- a/keystoneclient/tests/unit/test_hacking_checks.py +++ /dev/null @@ -1,47 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import textwrap - -import mock -import pep8 -import testtools - -from keystoneclient.hacking import checks -from keystoneclient.tests.unit import client_fixtures - - -class TestCheckOsloNamespaceImports(testtools.TestCase): - - # We are patching pep8 so that only the check under test is actually - # installed. - @mock.patch('pep8._checks', - {'physical_line': {}, 'logical_line': {}, 'tree': {}}) - def run_check(self, code): - pep8.register_check(checks.check_oslo_namespace_imports) - - lines = textwrap.dedent(code).strip().splitlines(True) - - checker = pep8.Checker(lines=lines) - checker.check_all() - checker.report._deferred_print.sort() - return checker.report._deferred_print - - def assert_has_errors(self, code, expected_errors=None): - actual_errors = [e[:3] for e in self.run_check(code)] - self.assertEqual(expected_errors or [], actual_errors) - - def test(self): - code_ex = self.useFixture(client_fixtures.HackingCode()) - code = code_ex.oslo_namespace_imports['code'] - errors = code_ex.oslo_namespace_imports['expected_errors'] - self.assert_has_errors(code, expected_errors=errors) diff --git a/keystoneclient/tests/unit/test_http.py b/keystoneclient/tests/unit/test_http.py index 436c374b7..4a8053fab 100644 --- a/keystoneclient/tests/unit/test_http.py +++ b/keystoneclient/tests/unit/test_http.py @@ -12,9 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +import io import logging -import six from testtools import matchers from keystoneclient import exceptions @@ -28,7 +28,7 @@ def get_client(): cl = httpclient.HTTPClient(username="username", password="password", - tenant_id="tenant", auth_url="auth_test") + project_id="tenant", auth_url="auth_test") return cl @@ -39,35 +39,28 @@ def get_authed_client(): return cl -class FakeLog(object): - def __init__(self): - self.warn_log = str() - self.debug_log = str() - - def warn(self, msg=None, *args, **kwargs): - self.warn_log = "%s\n%s" % (self.warn_log, (msg % args)) - - def debug(self, msg=None, *args, **kwargs): - self.debug_log = "%s\n%s" % (self.debug_log, (msg % args)) - - class ClientTest(utils.TestCase): TEST_URL = 'http://127.0.0.1:5000/hi' def test_unauthorized_client_requests(self): - cl = get_client() + # 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): - cl = get_authed_client() + # 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) - resp, body = cl.get("/hi") + 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) @@ -78,14 +71,18 @@ def test_get(self): self.assertEqual(body, {"hi": "there"}) def test_get_error_with_plaintext_resp(self): - cl = get_authed_client() + # 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): - cl = get_authed_client() + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = get_authed_client() err_response = { "error": { "code": 400, @@ -96,17 +93,21 @@ def test_get_error_with_json_resp(self): self.stub_url('GET', status_code=400, json=err_response) exc_raised = False try: - cl.get('/hi') + with self.deprecations.expect_deprecations_here(): + cl.get('/hi') except exceptions.BadRequest as exc: exc_raised = True - self.assertEqual(exc.message, "Error message string") + self.assertEqual(exc.message, "Error message string (HTTP 400)") self.assertTrue(exc_raised, 'Exception not raised.') def test_post(self): - cl = get_authed_client() + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = get_authed_client() self.stub_url('POST') - cl.post("/hi", body=[1, 2, 3]) + 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]') @@ -117,29 +118,21 @@ def test_post(self): def test_forwarded_for(self): ORIGINAL_IP = "10.100.100.1" - cl = httpclient.HTTPClient(username="username", password="password", - tenant_id="tenant", auth_url="auth_test", - original_ip=ORIGINAL_IP) + # 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') - cl.request(self.TEST_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) - def test_client_deprecated(self): - # Can resolve symbols from the keystoneclient.client module. - # keystoneclient.client was deprecated and renamed to - # keystoneclient.httpclient. This tests that keystoneclient.client - # can still be used. - - from keystoneclient import client - - # These statements will raise an AttributeError if the symbol isn't - # defined in the module. - - client.HTTPClient - class BasicRequestTests(utils.TestCase): @@ -147,7 +140,7 @@ class BasicRequestTests(utils.TestCase): def setUp(self): super(BasicRequestTests, self).setUp() - self.logger_message = six.moves.cStringIO() + self.logger_message = io.StringIO() handler = logging.StreamHandler(self.logger_message) handler.setLevel(logging.DEBUG) @@ -160,21 +153,24 @@ def setUp(self): self.addCleanup(self.logger.setLevel, level) def request(self, method='GET', response='Test Response', status_code=200, - url=None, **kwargs): + url=None, headers={}, **kwargs): if not url: url = self.url self.requests_mock.register_uri(method, url, text=response, - status_code=status_code) + status_code=status_code, + headers=headers) - return httpclient.request(url, method, **kwargs) + 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) + self.request(method=method, status_code=status, response=response, + headers={'Content-Type': 'application/json'}) self.assertEqual(self.requests_mock.last_request.method, method) @@ -193,16 +189,17 @@ def test_headers(self): self.request(headers=headers) - for k, v in six.iteritems(headers): + for k, v in headers.items(): self.assertRequestHeaderEqual(k, v) - for header in six.iteritems(headers): + 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) + 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 index e574d3750..315a17aaa 100644 --- a/keystoneclient/tests/unit/test_https.py +++ b/keystoneclient/tests/unit/test_https.py @@ -10,17 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. -import mock import requests +from unittest import mock from keystoneclient import httpclient from keystoneclient.tests.unit import utils -FAKE_RESPONSE = utils.TestResponse({ - "status_code": 200, - "text": '{"hi": "there"}', -}) +FAKE_RESPONSE = utils.test_response(json={'hi': 'there'}) REQUEST_URL = 'https://127.0.0.1:5000/hi' RESPONSE_BODY = '{"hi": "there"}' @@ -28,8 +25,8 @@ def get_client(): cl = httpclient.HTTPClient(username="username", password="password", - tenant_id="tenant", auth_url="auth_test", - cacert="ca.pem", key="key.pem", cert="cert.pem") + project_id="tenant", auth_url="auth_test", + cacert="ca.pem", cert=('cert.pem', "key.pem")) return cl @@ -42,19 +39,15 @@ def get_authed_client(): class ClientTest(utils.TestCase): - def setUp(self): - super(ClientTest, self).setUp() - self.request_patcher = mock.patch.object(requests, 'request', - self.mox.CreateMockAnything()) - self.request_patcher.start() - self.addCleanup(self.request_patcher.stop) - @mock.patch.object(requests, 'request') def test_get(self, MOCK_REQUEST): MOCK_REQUEST.return_value = FAKE_RESPONSE - cl = get_authed_client() + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = get_authed_client() - resp, body = cl.get("/hi") + 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 @@ -71,9 +64,12 @@ def test_get(self, MOCK_REQUEST): @mock.patch.object(requests, 'request') def test_post(self, MOCK_REQUEST): MOCK_REQUEST.return_value = FAKE_RESPONSE - cl = get_authed_client() + # Creating a HTTPClient not using session is deprecated. + with self.deprecations.expect_deprecations_here(): + cl = get_authed_client() - cl.post("/hi", body=[1, 2, 3]) + 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 @@ -88,13 +84,16 @@ def test_post(self, MOCK_REQUEST): @mock.patch.object(requests, 'request') def test_post_auth(self, MOCK_REQUEST): MOCK_REQUEST.return_value = FAKE_RESPONSE - cl = httpclient.HTTPClient( - username="username", password="password", tenant_id="tenant", - auth_url="auth_test", cacert="ca.pem", key="key.pem", - cert="cert.pem") + # 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" - cl.post("/hi", body=[1, 2, 3]) + 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 diff --git a/keystoneclient/tests/unit/test_keyring.py b/keystoneclient/tests/unit/test_keyring.py index a54009e9c..0a5bf7150 100644 --- a/keystoneclient/tests/unit/test_keyring.py +++ b/keystoneclient/tests/unit/test_keyring.py @@ -11,14 +11,15 @@ # under the License. import datetime +from unittest import mock -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 @@ -53,6 +54,7 @@ class MemoryKeyring(keyring.backend.KeyringBackend): 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 @@ -83,11 +85,15 @@ def set_password(self, service, username, password): keyring.set_keyring(self.memory_keyring) def test_no_keyring_key(self): - """Ensure that if we don't have use_keyring set in the client that + """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. """ - cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, - tenant_id=TENANT_ID, auth_url=AUTH_URL) + # 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' @@ -103,8 +109,10 @@ def test_no_keyring_key(self): self.assertFalse(self.memory_keyring.set_password_called) def test_build_keyring_key(self): - cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, - tenant_id=TENANT_ID, auth_url=AUTH_URL) + # 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, @@ -117,14 +125,16 @@ def test_build_keyring_key(self): (AUTH_URL, TENANT_ID, TENANT, TOKEN, USERNAME)) def test_set_and_get_keyring_expired(self): - cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, - tenant_id=TENANT_ID, auth_url=AUTH_URL, - use_keyring=True) + # 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'] = timeutils.isotime(expired) + 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 @@ -145,14 +155,16 @@ def test_set_and_get_keyring_expired(self): PROJECT_SCOPED_TOKEN['access']['token']['expires']) def test_get_keyring(self): - cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, - tenant_id=TENANT_ID, auth_url=AUTH_URL, - use_keyring=True) + # 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'] = timeutils.isotime(future) + 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 @@ -161,9 +173,11 @@ def test_get_keyring(self): self.assertTrue(self.memory_keyring.fetched) def test_set_keyring(self): - cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, - tenant_id=TENANT_ID, auth_url=AUTH_URL, - use_keyring=True) + # 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' diff --git a/keystoneclient/tests/unit/test_memcache_crypt.py b/keystoneclient/tests/unit/test_memcache_crypt.py deleted file mode 100644 index be07b24ea..000000000 --- a/keystoneclient/tests/unit/test_memcache_crypt.py +++ /dev/null @@ -1,97 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import six -import testtools - -from keystoneclient.middleware import memcache_crypt - - -class MemcacheCryptPositiveTests(testtools.TestCase): - def _setup_keys(self, strategy): - return memcache_crypt.derive_keys(b'token', b'secret', strategy) - - def test_constant_time_compare(self): - # make sure it works as a compare, the "constant time" aspect - # isn't appropriate to test in unittests - ctc = memcache_crypt.constant_time_compare - self.assertTrue(ctc('abcd', 'abcd')) - self.assertTrue(ctc('', '')) - self.assertFalse(ctc('abcd', 'efgh')) - self.assertFalse(ctc('abc', 'abcd')) - self.assertFalse(ctc('abc', 'abc\x00')) - self.assertFalse(ctc('', 'abc')) - - # For Python 3, we want to test these functions with both str and bytes - # as input. - if six.PY3: - self.assertTrue(ctc(b'abcd', b'abcd')) - self.assertTrue(ctc(b'', b'')) - self.assertFalse(ctc(b'abcd', b'efgh')) - self.assertFalse(ctc(b'abc', b'abcd')) - self.assertFalse(ctc(b'abc', b'abc\x00')) - self.assertFalse(ctc(b'', b'abc')) - - def test_derive_keys(self): - keys = self._setup_keys(b'strategy') - self.assertEqual(len(keys['ENCRYPTION']), - len(keys['CACHE_KEY'])) - self.assertEqual(len(keys['CACHE_KEY']), - len(keys['MAC'])) - self.assertNotEqual(keys['ENCRYPTION'], - keys['MAC']) - self.assertIn('strategy', keys.keys()) - - def test_key_strategy_diff(self): - k1 = self._setup_keys(b'MAC') - k2 = self._setup_keys(b'ENCRYPT') - self.assertNotEqual(k1, k2) - - def test_sign_data(self): - keys = self._setup_keys(b'MAC') - sig = memcache_crypt.sign_data(keys['MAC'], b'data') - self.assertEqual(len(sig), memcache_crypt.DIGEST_LENGTH_B64) - - def test_encryption(self): - keys = self._setup_keys(b'ENCRYPT') - # what you put in is what you get out - for data in [b'data', b'1234567890123456', b'\x00\xFF' * 13 - ] + [six.int2byte(x % 256) * x for x in range(768)]: - crypt = memcache_crypt.encrypt_data(keys['ENCRYPTION'], data) - decrypt = memcache_crypt.decrypt_data(keys['ENCRYPTION'], crypt) - self.assertEqual(data, decrypt) - self.assertRaises(memcache_crypt.DecryptError, - memcache_crypt.decrypt_data, - keys['ENCRYPTION'], crypt[:-1]) - - def test_protect_wrappers(self): - data = b'My Pretty Little Data' - for strategy in [b'MAC', b'ENCRYPT']: - keys = self._setup_keys(strategy) - protected = memcache_crypt.protect_data(keys, data) - self.assertNotEqual(protected, data) - if strategy == b'ENCRYPT': - self.assertNotIn(data, protected) - unprotected = memcache_crypt.unprotect_data(keys, protected) - self.assertEqual(data, unprotected) - self.assertRaises(memcache_crypt.InvalidMacError, - memcache_crypt.unprotect_data, - keys, protected[:-1]) - self.assertIsNone(memcache_crypt.unprotect_data(keys, None)) - - def test_no_pycrypt(self): - aes = memcache_crypt.AES - memcache_crypt.AES = None - self.assertRaises(memcache_crypt.CryptoUnavailableError, - memcache_crypt.encrypt_data, 'token', 'secret', - 'data') - memcache_crypt.AES = aes diff --git a/keystoneclient/tests/unit/test_s3_token_middleware.py b/keystoneclient/tests/unit/test_s3_token_middleware.py deleted file mode 100644 index dfb4406ed..000000000 --- a/keystoneclient/tests/unit/test_s3_token_middleware.py +++ /dev/null @@ -1,259 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import mock -from oslo_serialization import jsonutils -import requests -import six -import testtools -import webob - -from keystoneclient.middleware import s3_token -from keystoneclient.tests.unit import utils - - -GOOD_RESPONSE = {'access': {'token': {'id': 'TOKEN_ID', - 'tenant': {'id': 'TENANT_ID'}}}} - - -class FakeApp(object): - """This represents a WSGI app protected by the auth_token middleware.""" - def __call__(self, env, start_response): - resp = webob.Response() - resp.environ = env - return resp(env, start_response) - - -class S3TokenMiddlewareTestBase(utils.TestCase): - - TEST_PROTOCOL = 'https' - TEST_HOST = 'fakehost' - TEST_PORT = 35357 - TEST_URL = '%s://%s:%d/v2.0/s3tokens' % (TEST_PROTOCOL, - TEST_HOST, - TEST_PORT) - - def setUp(self): - super(S3TokenMiddlewareTestBase, self).setUp() - - self.conf = { - 'auth_host': self.TEST_HOST, - 'auth_port': self.TEST_PORT, - 'auth_protocol': self.TEST_PROTOCOL, - } - - def start_fake_response(self, status, headers): - self.response_status = int(status.split(' ', 1)[0]) - self.response_headers = dict(headers) - - -class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase): - - def setUp(self): - super(S3TokenMiddlewareTestGood, self).setUp() - self.middleware = s3_token.S3Token(FakeApp(), self.conf) - - self.requests_mock.post(self.TEST_URL, - status_code=201, - json=GOOD_RESPONSE) - - # Ignore the request and pass to the next middleware in the - # pipeline if no path has been specified. - def test_no_path_request(self): - req = webob.Request.blank('/') - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 200) - - # Ignore the request and pass to the next middleware in the - # pipeline if no Authorization header has been specified - def test_without_authorization(self): - req = webob.Request.blank('/v1/AUTH_cfa/c/o') - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 200) - - def test_without_auth_storage_token(self): - req = webob.Request.blank('/v1/AUTH_cfa/c/o') - req.headers['Authorization'] = 'badboy' - self.middleware(req.environ, self.start_fake_response) - self.assertEqual(self.response_status, 200) - - def test_authorized(self): - req = webob.Request.blank('/v1/AUTH_cfa/c/o') - req.headers['Authorization'] = 'access:signature' - req.headers['X-Storage-Token'] = 'token' - req.get_response(self.middleware) - self.assertTrue(req.path.startswith('/v1/AUTH_TENANT_ID')) - self.assertEqual(req.headers['X-Auth-Token'], 'TOKEN_ID') - - def test_authorized_http(self): - TEST_URL = 'http://%s:%d/v2.0/s3tokens' % (self.TEST_HOST, - self.TEST_PORT) - - self.requests_mock.post(TEST_URL, status_code=201, json=GOOD_RESPONSE) - - self.middleware = ( - s3_token.filter_factory({'auth_protocol': 'http', - 'auth_host': self.TEST_HOST, - 'auth_port': self.TEST_PORT})(FakeApp())) - req = webob.Request.blank('/v1/AUTH_cfa/c/o') - req.headers['Authorization'] = 'access:signature' - req.headers['X-Storage-Token'] = 'token' - req.get_response(self.middleware) - self.assertTrue(req.path.startswith('/v1/AUTH_TENANT_ID')) - self.assertEqual(req.headers['X-Auth-Token'], 'TOKEN_ID') - - def test_authorization_nova_toconnect(self): - req = webob.Request.blank('/v1/AUTH_swiftint/c/o') - req.headers['Authorization'] = 'access:FORCED_TENANT_ID:signature' - req.headers['X-Storage-Token'] = 'token' - req.get_response(self.middleware) - path = req.environ['PATH_INFO'] - self.assertTrue(path.startswith('/v1/AUTH_FORCED_TENANT_ID')) - - @mock.patch.object(requests, 'post') - def test_insecure(self, MOCK_REQUEST): - self.middleware = ( - s3_token.filter_factory({'insecure': 'True'})(FakeApp())) - - text_return_value = jsonutils.dumps(GOOD_RESPONSE) - if six.PY3: - text_return_value = text_return_value.encode() - MOCK_REQUEST.return_value = utils.TestResponse({ - 'status_code': 201, - 'text': text_return_value}) - - req = webob.Request.blank('/v1/AUTH_cfa/c/o') - req.headers['Authorization'] = 'access:signature' - req.headers['X-Storage-Token'] = 'token' - req.get_response(self.middleware) - - self.assertTrue(MOCK_REQUEST.called) - mock_args, mock_kwargs = MOCK_REQUEST.call_args - self.assertIs(mock_kwargs['verify'], False) - - def test_insecure_option(self): - # insecure is passed as a string. - - # Some non-secure values. - true_values = ['true', 'True', '1', 'yes'] - for val in true_values: - config = {'insecure': val, 'certfile': 'false_ind'} - middleware = s3_token.filter_factory(config)(FakeApp()) - self.assertIs(False, middleware.verify) - - # Some "secure" values, including unexpected value. - false_values = ['false', 'False', '0', 'no', 'someweirdvalue'] - for val in false_values: - config = {'insecure': val, 'certfile': 'false_ind'} - middleware = s3_token.filter_factory(config)(FakeApp()) - self.assertEqual('false_ind', middleware.verify) - - # Default is secure. - config = {'certfile': 'false_ind'} - middleware = s3_token.filter_factory(config)(FakeApp()) - self.assertIs('false_ind', middleware.verify) - - -class S3TokenMiddlewareTestBad(S3TokenMiddlewareTestBase): - def setUp(self): - super(S3TokenMiddlewareTestBad, self).setUp() - self.middleware = s3_token.S3Token(FakeApp(), self.conf) - - def test_unauthorized_token(self): - ret = {"error": - {"message": "EC2 access key not found.", - "code": 401, - "title": "Unauthorized"}} - self.requests_mock.post(self.TEST_URL, status_code=403, json=ret) - req = webob.Request.blank('/v1/AUTH_cfa/c/o') - req.headers['Authorization'] = 'access:signature' - req.headers['X-Storage-Token'] = 'token' - resp = req.get_response(self.middleware) - s3_denied_req = self.middleware.deny_request('AccessDenied') - self.assertEqual(resp.body, s3_denied_req.body) - self.assertEqual(resp.status_int, s3_denied_req.status_int) - - def test_bogus_authorization(self): - req = webob.Request.blank('/v1/AUTH_cfa/c/o') - req.headers['Authorization'] = 'badboy' - req.headers['X-Storage-Token'] = 'token' - resp = req.get_response(self.middleware) - self.assertEqual(resp.status_int, 400) - s3_invalid_req = self.middleware.deny_request('InvalidURI') - self.assertEqual(resp.body, s3_invalid_req.body) - self.assertEqual(resp.status_int, s3_invalid_req.status_int) - - def test_fail_to_connect_to_keystone(self): - with mock.patch.object(self.middleware, '_json_request') as o: - s3_invalid_req = self.middleware.deny_request('InvalidURI') - o.side_effect = s3_token.ServiceError(s3_invalid_req) - - req = webob.Request.blank('/v1/AUTH_cfa/c/o') - req.headers['Authorization'] = 'access:signature' - req.headers['X-Storage-Token'] = 'token' - resp = req.get_response(self.middleware) - self.assertEqual(resp.body, s3_invalid_req.body) - self.assertEqual(resp.status_int, s3_invalid_req.status_int) - - def test_bad_reply(self): - self.requests_mock.post(self.TEST_URL, - status_code=201, - text="") - - req = webob.Request.blank('/v1/AUTH_cfa/c/o') - req.headers['Authorization'] = 'access:signature' - req.headers['X-Storage-Token'] = 'token' - resp = req.get_response(self.middleware) - s3_invalid_req = self.middleware.deny_request('InvalidURI') - self.assertEqual(resp.body, s3_invalid_req.body) - self.assertEqual(resp.status_int, s3_invalid_req.status_int) - - -class S3TokenMiddlewareTestUtil(testtools.TestCase): - def test_split_path_failed(self): - self.assertRaises(ValueError, s3_token.split_path, '') - self.assertRaises(ValueError, s3_token.split_path, '/') - self.assertRaises(ValueError, s3_token.split_path, '//') - self.assertRaises(ValueError, s3_token.split_path, '//a') - self.assertRaises(ValueError, s3_token.split_path, '/a/c') - self.assertRaises(ValueError, s3_token.split_path, '//c') - self.assertRaises(ValueError, s3_token.split_path, '/a/c/') - self.assertRaises(ValueError, s3_token.split_path, '/a//') - self.assertRaises(ValueError, s3_token.split_path, '/a', 2) - self.assertRaises(ValueError, s3_token.split_path, '/a', 2, 3) - self.assertRaises(ValueError, s3_token.split_path, '/a', 2, 3, True) - self.assertRaises(ValueError, s3_token.split_path, '/a/c/o/r', 3, 3) - self.assertRaises(ValueError, s3_token.split_path, '/a', 5, 4) - - def test_split_path_success(self): - self.assertEqual(s3_token.split_path('/a'), ['a']) - self.assertEqual(s3_token.split_path('/a/'), ['a']) - self.assertEqual(s3_token.split_path('/a/c', 2), ['a', 'c']) - self.assertEqual(s3_token.split_path('/a/c/o', 3), ['a', 'c', 'o']) - self.assertEqual(s3_token.split_path('/a/c/o/r', 3, 3, True), - ['a', 'c', 'o/r']) - self.assertEqual(s3_token.split_path('/a/c', 2, 3, True), - ['a', 'c', None]) - self.assertEqual(s3_token.split_path('/a/c/', 2), ['a', 'c']) - self.assertEqual(s3_token.split_path('/a/c/', 2, 3), ['a', 'c', '']) - - def test_split_path_invalid_path(self): - try: - s3_token.split_path('o\nn e', 2) - except ValueError as err: - self.assertEqual(str(err), 'Invalid path: o%0An%20e') - try: - s3_token.split_path('o\nn e', 2, 3, True) - except ValueError as err: - self.assertEqual(str(err), 'Invalid path: o%0An%20e') diff --git a/keystoneclient/tests/unit/test_session.py b/keystoneclient/tests/unit/test_session.py index e0b6fb5ab..71a8e2743 100644 --- a/keystoneclient/tests/unit/test_session.py +++ b/keystoneclient/tests/unit/test_session.py @@ -1,3 +1,5 @@ +# -*- 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 @@ -11,16 +13,16 @@ # under the License. import argparse +from io import StringIO import itertools import logging +from unittest import mock import uuid -import mock from oslo_config import cfg from oslo_config import fixture as config from oslo_serialization import jsonutils import requests -import six from testtools import matchers from keystoneclient import adapter @@ -35,6 +37,10 @@ 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') @@ -113,7 +119,7 @@ def test_http_session_opts(self): session = client_session.Session(cert='cert.pem', timeout=5, verify='certs') - FAKE_RESP = utils.TestResponse({'status_code': 200, 'text': 'resp'}) + FAKE_RESP = utils.test_response(text='resp') RESP = mock.Mock(return_value=FAKE_RESP) with mock.patch.object(session.session, 'request', RESP) as mocked: @@ -140,17 +146,19 @@ def test_server_error(self): session.get, self.TEST_URL) def test_session_debug_output(self): - """Test request and response headers in debug logs + """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'} + 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, } - body = 'BODYRESPONSE' - data = 'BODYDATA' + '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) @@ -163,28 +171,83 @@ def test_session_debug_output(self): self.assertIn(body, self.logger.output) self.assertIn("'%s'" % data, self.logger.output) - for k, v in six.iteritems(headers): + 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 six.iteritems(security_headers): + 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""" - + """Test that output is logged even for failed requests.""" session = client_session.Session() - body = uuid.uuid4().hex + body = {uuid.uuid4().hex: uuid.uuid4().hex} - self.stub_url('GET', text=body, status_code=400) + 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' @@ -244,10 +307,79 @@ def _ssl_error(request, context): # 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) + 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): @@ -260,8 +392,14 @@ class RedirectTests(utils.TestCase): 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={}, final_kwargs={}): + 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:]): @@ -336,7 +474,8 @@ class ConstructSessionFromArgsTests(utils.TestCase): def _s(self, k=None, **kwargs): k = k or kwargs - return client_session.Session.construct(k) + with self.deprecations.expect_deprecations_here(): + return client_session.Session.construct(k) def test_verify(self): self.assertFalse(self._s(insecure=True).verify) @@ -364,7 +503,7 @@ class AuthPlugin(base.BaseAuthPlugin): Takes Parameters such that it can throw exceptions at the right times. """ - TEST_TOKEN = 'aToken' + TEST_TOKEN = utils.TestCase.TEST_TOKEN TEST_USER_ID = 'aUser' TEST_PROJECT_ID = 'aProject' @@ -414,7 +553,7 @@ def __init__(self, invalidate=True): def get_token(self, session): self.get_token_called = True - return 'aToken' + return utils.TestCase.TEST_TOKEN def get_endpoint(self, session, **kwargs): self.get_endpoint_called = True @@ -431,6 +570,10 @@ 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] @@ -445,7 +588,7 @@ def test_auth_plugin_default_with_plugin(self): auth = AuthPlugin() sess = client_session.Session(auth=auth) resp = sess.get(self.TEST_URL) - self.assertDictEqual(resp.json(), self.TEST_JSON) + self.assertEqual(resp.json(), self.TEST_JSON) self.assertRequestHeaderEqual('X-Auth-Token', AuthPlugin.TEST_TOKEN) @@ -455,7 +598,7 @@ def test_auth_plugin_disable(self): auth = AuthPlugin() sess = client_session.Session(auth=auth) resp = sess.get(self.TEST_URL, authenticated=False) - self.assertDictEqual(resp.json(), self.TEST_JSON) + self.assertEqual(resp.json(), self.TEST_JSON) self.assertRequestHeaderEqual('X-Auth-Token', None) @@ -552,7 +695,7 @@ def test_requests_auth_plugin(self): requests_auth = object() - FAKE_RESP = utils.TestResponse({'status_code': 200, 'text': 'resp'}) + FAKE_RESP = utils.test_response(text='resp') RESP = mock.Mock(return_value=FAKE_RESP) with mock.patch.object(sess.session, 'request', RESP) as mocked: @@ -644,28 +787,29 @@ def test_logger_object_passed(self): logger.setLevel(logging.DEBUG) logger.propagate = False - io = six.StringIO() + io = StringIO() handler = logging.StreamHandler(io) logger.addHandler(handler) auth = AuthPlugin() sess = client_session.Session(auth=auth) - response = uuid.uuid4().hex + response = {uuid.uuid4().hex: uuid.uuid4().hex} self.stub_url('GET', - text=response, - headers={'Content-Type': 'text/html'}) + json=response, + headers={'Content-Type': 'application/json'}) resp = sess.get(self.TEST_URL, logger=logger) - self.assertEqual(response, resp.text) + self.assertEqual(response, resp.json()) output = io.getvalue() self.assertIn(self.TEST_URL, output) - self.assertIn(response, output) + self.assertIn(list(response.keys())[0], output) + self.assertIn(list(response.values())[0], output) - self.assertNotIn(self.TEST_URL, self.logger.output) - self.assertNotIn(response, self.logger.output) + self.assertNotIn(list(response.keys())[0], self.logger.output) + self.assertNotIn(list(response.values())[0], self.logger.output) class AdapterTest(utils.TestCase): @@ -679,6 +823,10 @@ class AdapterTest(utils.TestCase): 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() @@ -835,7 +983,7 @@ def test_logger_object_passed(self): logger.setLevel(logging.DEBUG) logger.propagate = False - io = six.StringIO() + io = StringIO() handler = logging.StreamHandler(io) logger.addHandler(handler) @@ -843,21 +991,22 @@ def test_logger_object_passed(self): sess = client_session.Session(auth=auth) adpt = adapter.Adapter(sess, auth=auth, logger=logger) - response = uuid.uuid4().hex + response = {uuid.uuid4().hex: uuid.uuid4().hex} - self.stub_url('GET', text=response, - headers={'Content-Type': 'text/html'}) + self.stub_url('GET', json=response, + headers={'Content-Type': 'application/json'}) resp = adpt.get(self.TEST_URL, logger=logger) - self.assertEqual(response, resp.text) + self.assertEqual(response, resp.json()) output = io.getvalue() self.assertIn(self.TEST_URL, output) - self.assertIn(response, output) + self.assertIn(list(response.keys())[0], output) + self.assertIn(list(response.values())[0], output) - self.assertNotIn(self.TEST_URL, self.logger.output) - self.assertNotIn(response, self.logger.output) + self.assertNotIn(list(response.keys())[0], self.logger.output) + self.assertNotIn(list(response.values())[0], self.logger.output) class ConfLoadingTests(utils.TestCase): @@ -876,10 +1025,11 @@ def config(self, **kwargs): self.conf_fixture.config(**kwargs) def get_session(self, **kwargs): - return client_session.Session.load_from_conf_options( - self.conf_fixture.conf, - self.GROUP, - **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) @@ -929,7 +1079,8 @@ def setUp(self): def get_session(self, val, **kwargs): args = self.parser.parse_args(val.split()) - return client_session.Session.load_from_cli_options(args, **kwargs) + 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') diff --git a/keystoneclient/tests/unit/test_shell.py b/keystoneclient/tests/unit/test_shell.py deleted file mode 100644 index 452f9e793..000000000 --- a/keystoneclient/tests/unit/test_shell.py +++ /dev/null @@ -1,532 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import argparse -import json -import logging -import os -import sys -import uuid - -import fixtures -import mock -import six -import testtools -from testtools import matchers - -from keystoneclient import exceptions -from keystoneclient import session -from keystoneclient import shell as openstack_shell -from keystoneclient.tests.unit import utils -from keystoneclient.v2_0 import shell as shell_v2_0 - - -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/' - - -# Make a fake shell object, a helping wrapper to call it -def shell(cmd): - openstack_shell.OpenStackIdentityShell().main(cmd.split()) - - -class NoExitArgumentParser(argparse.ArgumentParser): - def error(self, message): - raise exceptions.CommandError(message) - - -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, - } - - def _tolerant_shell(self, cmd): - t_shell = openstack_shell.OpenStackIdentityShell(NoExitArgumentParser) - t_shell.main(cmd.split()) - - # Patch os.environ to avoid required auth info. - def setUp(self): - - super(ShellTest, self).setUp() - for var in os.environ: - if var.startswith("OS_"): - self.useFixture(fixtures.EnvironmentVariable(var, "")) - - for var in self.FAKE_ENV: - self.useFixture(fixtures.EnvironmentVariable(var, - self.FAKE_ENV[var])) - - def test_help_unknown_command(self): - self.assertRaises(exceptions.CommandError, shell, 'help %s' - % uuid.uuid4().hex) - - def shell(self, argstr): - orig = sys.stdout - clean_env = {} - _old_env, os.environ = os.environ, clean_env.copy() - try: - sys.stdout = six.StringIO() - _shell = openstack_shell.OpenStackIdentityShell() - _shell.main(argstr.split()) - except SystemExit: - exc_type, exc_value, exc_traceback = sys.exc_info() - self.assertEqual(exc_value.code, 0) - finally: - out = sys.stdout.getvalue() - sys.stdout.close() - sys.stdout = orig - os.environ = _old_env - return out - - def test_help_no_args(self): - do_tenant_mock = mock.MagicMock() - with mock.patch('keystoneclient.shell.OpenStackIdentityShell.do_help', - do_tenant_mock): - self.shell('') - assert do_tenant_mock.called - - def test_help(self): - required = 'usage:' - help_text = self.shell('help') - self.assertThat(help_text, - matchers.MatchesRegex(required)) - - def test_help_command(self): - required = 'usage: keystone user-create' - help_text = self.shell('help user-create') - self.assertThat(help_text, - matchers.MatchesRegex(required)) - - def test_help_command_with_no_action_choices(self): - required = 'usage: keystone user-update' - help_text = self.shell('help user-update') - self.assertThat(help_text, - matchers.MatchesRegex(required)) - - def test_auth_no_credentials(self): - with testtools.ExpectedException( - exceptions.CommandError, 'Expecting'): - self.shell('user-list') - - def test_debug(self): - logging_mock = mock.MagicMock() - with mock.patch('logging.basicConfig', logging_mock): - self.assertRaises(exceptions.CommandError, - self.shell, '--debug user-list') - self.assertTrue(logging_mock.called) - self.assertEqual([(), {'level': logging.DEBUG}], - list(logging_mock.call_args)) - - def test_auth_password_authurl_no_username(self): - with testtools.ExpectedException( - exceptions.CommandError, - 'Expecting a username provided via either'): - self.shell('--os-password=%s --os-auth-url=%s user-list' - % (uuid.uuid4().hex, uuid.uuid4().hex)) - - def test_auth_username_password_no_authurl(self): - with testtools.ExpectedException( - exceptions.CommandError, 'Expecting an auth URL via either'): - self.shell('--os-password=%s --os-username=%s user-list' - % (uuid.uuid4().hex, uuid.uuid4().hex)) - - def test_token_no_endpoint(self): - with testtools.ExpectedException( - exceptions.CommandError, 'Expecting an endpoint provided'): - self.shell('--os-token=%s user-list' % uuid.uuid4().hex) - - def test_endpoint_no_token(self): - with testtools.ExpectedException( - exceptions.CommandError, 'Expecting a token provided'): - self.shell('--os-endpoint=http://10.0.0.1:5000/v2.0/ user-list') - - 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)])) - - # Test os-identity-api-version fall back to 2.0 - shell('--os-identity-api-version 3.0 user-list') - assert do_tenant_mock.called - self.assertTrue(b.os_identity_api_version, '2.0') - - 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=secret --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', 'secret', 'true') - self.assertTrue(all([x == y for x, y in zip(actual, expect)])) - - # New-style options - # Test case with one --tenant args present: ec2 creds - shell('user-create --name=foo ' - '--pass=secret --tenant=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, b.name, b.passwd, b.enabled) - expect = ('BARRR', 'foo', 'secret', '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=secret --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, b.name, b.passwd, b.enabled) - expect = ('BARRR', 'foo', 'secret', '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=secret --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', 'secret', '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=secret --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, b.name, b.passwd, b.enabled) - expect = ('BARRR', 'foo', 'secret', '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_timeout_parse_invalid_type(self): - for f in ['foobar', 'xyz']: - cmd = '--timeout %s endpoint-create' % (f) - self.assertRaises(exceptions.CommandError, - self._tolerant_shell, cmd) - - def test_timeout_parse_invalid_number(self): - for f in [-1, 0]: - cmd = '--timeout %s endpoint-create' % (f) - self.assertRaises(exceptions.CommandError, - self._tolerant_shell, cmd) - - def test_do_timeout(self): - response_mock = mock.MagicMock() - response_mock.status_code = 200 - response_mock.text = json.dumps({ - 'endpoints': [], - }) - request_mock = mock.MagicMock(return_value=response_mock) - with mock.patch.object(session.requests, 'request', - request_mock): - shell(('--timeout 2 --os-token=blah --os-endpoint=blah' - ' --os-auth-url=blah.com endpoint-list')) - request_mock.assert_called_with(mock.ANY, mock.ANY, - timeout=2, - allow_redirects=False, - headers=mock.ANY, - verify=mock.ANY) - - 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, 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, 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)])) - - # New-style options - # Test create args - shell('endpoint-create ' - '--service=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, 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)])) - - def test_shell_keyboard_interrupt(self): - shell_mock = mock.MagicMock() - with mock.patch('keystoneclient.shell.OpenStackIdentityShell.main', - shell_mock): - try: - shell_mock.side_effect = KeyboardInterrupt() - openstack_shell.main() - except SystemExit as ex: - self.assertEqual(130, ex.code) diff --git a/keystoneclient/tests/unit/test_utils.py b/keystoneclient/tests/unit/test_utils.py index 8f0de9b0f..2aa5e92be 100644 --- a/keystoneclient/tests/unit/test_utils.py +++ b/keystoneclient/tests/unit/test_utils.py @@ -10,14 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import logging -import sys - -import six +from keystoneauth1 import exceptions as ksa_exceptions import testresources from testtools import matchers -from keystoneclient import exceptions + +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 @@ -34,7 +32,7 @@ class FakeManager(object): resources = { '1234': {'name': 'entity_one'}, '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0': {'name': 'entity_two'}, - '\xe3\x82\xbdtest': {'name': u'\u30bdtest'}, + '\xe3\x82\xbdtest': {'name': '\u30bdtest'}, '5678': {'name': '9876'} } @@ -42,16 +40,16 @@ def get(self, resource_id): try: return self.resources[str(resource_id)] except KeyError: - raise exceptions.NotFound(resource_id) + raise ksa_exceptions.NotFound(resource_id) def find(self, name=None): if name == '9999': # NOTE(morganfainberg): special case that raises NoUniqueMatch. - raise exceptions.NoUniqueMatch() + raise ksc_exceptions.NoUniqueMatch() for resource_id, resource in self.resources.items(): if resource['name'] == str(name): return resource - raise exceptions.NotFound(name) + raise ksa_exceptions.NotFound(name) class FindResourceTestCase(test_utils.TestCase): @@ -61,7 +59,7 @@ def setUp(self): self.manager = FakeManager() def test_find_none(self): - self.assertRaises(exceptions.CommandError, + self.assertRaises(ksc_exceptions.CommandError, utils.find_resource, self.manager, 'asdf') @@ -93,7 +91,7 @@ def test_find_by_int_name(self): self.assertEqual(output, self.manager.resources['5678']) def test_find_no_unique_match(self): - self.assertRaises(exceptions.CommandError, + self.assertRaises(ksc_exceptions.CommandError, utils.find_resource, self.manager, 9999) @@ -104,111 +102,6 @@ def __init__(self, name): self.name = name -class PrintTestCase(test_utils.TestCase): - def setUp(self): - super(PrintTestCase, self).setUp() - self.old_stdout = sys.stdout - self.stdout = six.moves.cStringIO() - self.addCleanup(setattr, self, 'stdout', None) - sys.stdout = self.stdout - self.addCleanup(setattr, sys, 'stdout', self.old_stdout) - - def test_print_list_unicode(self): - name = six.u('\u540d\u5b57') - objs = [FakeObject(name)] - # NOTE(Jeffrey4l) If the text's encode is proper, this method will not - # raise UnicodeEncodeError exceptions - utils.print_list(objs, ['name']) - output = self.stdout.getvalue() - # In Python 2, output will be bytes, while in Python 3, it will not. - # Let's decode the value if needed. - if isinstance(output, six.binary_type): - output = output.decode('utf-8') - self.assertIn(name, output) - - def test_print_dict_unicode(self): - name = six.u('\u540d\u5b57') - utils.print_dict({'name': name}) - output = self.stdout.getvalue() - # In Python 2, output will be bytes, while in Python 3, it will not. - # Let's decode the value if needed. - if isinstance(output, six.binary_type): - output = output.decode('utf-8') - self.assertIn(name, output) - - -class TestPositional(test_utils.TestCase): - - @utils.positional(1) - def no_vars(self): - # positional doesn't enforce anything here - return True - - @utils.positional(3, utils.positional.EXCEPT) - def mixed_except(self, arg, kwarg1=None, kwarg2=None): - # self, arg, and kwarg1 may be passed positionally - return (arg, kwarg1, kwarg2) - - @utils.positional(3, utils.positional.WARN) - def mixed_warn(self, arg, kwarg1=None, kwarg2=None): - # self, arg, and kwarg1 may be passed positionally, only a warning - # is emitted - return (arg, kwarg1, kwarg2) - - def test_nothing(self): - self.assertTrue(self.no_vars()) - - def test_mixed_except(self): - self.assertEqual((1, 2, 3), self.mixed_except(1, 2, kwarg2=3)) - self.assertEqual((1, 2, 3), self.mixed_except(1, kwarg1=2, kwarg2=3)) - self.assertEqual((1, None, None), self.mixed_except(1)) - self.assertRaises(TypeError, self.mixed_except, 1, 2, 3) - - def test_mixed_warn(self): - logger_message = six.moves.cStringIO() - handler = logging.StreamHandler(logger_message) - handler.setLevel(logging.DEBUG) - - logger = logging.getLogger(utils.__name__) - level = logger.getEffectiveLevel() - logger.setLevel(logging.DEBUG) - logger.addHandler(handler) - - self.addCleanup(logger.removeHandler, handler) - self.addCleanup(logger.setLevel, level) - - self.mixed_warn(1, 2, 3) - - self.assertIn('takes at most 3 positional', logger_message.getvalue()) - - @utils.positional(enforcement=utils.positional.EXCEPT) - def inspect_func(self, arg, kwarg=None): - return (arg, kwarg) - - def test_inspect_positions(self): - self.assertEqual((1, None), self.inspect_func(1)) - self.assertEqual((1, 2), self.inspect_func(1, kwarg=2)) - self.assertRaises(TypeError, self.inspect_func) - self.assertRaises(TypeError, self.inspect_func, 1, 2) - - @utils.positional.classmethod(1) - def class_method(cls, a, b): - return (cls, a, b) - - @utils.positional.method(1) - def normal_method(self, a, b): - self.assertIsInstance(self, TestPositional) - return (self, a, b) - - def test_class_method(self): - self.assertEqual((TestPositional, 1, 2), self.class_method(1, b=2)) - self.assertRaises(TypeError, self.class_method, 1, 2) - - def test_normal_method(self): - self.assertEqual((self, 1, 2), self.normal_method(1, b=2)) - self.assertRaises(TypeError, self.normal_method, 1, 2) - - class HashSignedTokenTestCase(test_utils.TestCase, testresources.ResourcedTestCase): """Unit tests for utils.hash_signed_token().""" @@ -218,8 +111,7 @@ class HashSignedTokenTestCase(test_utils.TestCase, def test_default_md5(self): """The default hash method is md5.""" token = self.examples.SIGNED_TOKEN_SCOPED - if six.PY3: - token = token.encode('utf-8') + 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)) @@ -229,8 +121,7 @@ def test_default_md5(self): def test_sha256(self): """Can also hash with sha256.""" token = self.examples.SIGNED_TOKEN_SCOPED - if six.PY3: - token = token.encode('utf-8') + 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)) diff --git a/keystoneclient/tests/unit/utils.py b/keystoneclient/tests/unit/utils.py index b3405fbcb..4463213b1 100644 --- a/keystoneclient/tests/unit/utils.py +++ b/keystoneclient/tests/unit/utils.py @@ -12,50 +12,42 @@ import logging import sys -import time +import urllib.parse as urlparse import uuid import fixtures -import mock -from mox3 import mox from oslo_serialization import jsonutils import requests +import requests_mock from requests_mock.contrib import fixture -import six -from six.moves.urllib import parse as urlparse +import testscenarios import testtools +from keystoneclient.tests.unit import client_fixtures + class TestCase(testtools.TestCase): - TEST_DOMAIN_ID = '1' - TEST_DOMAIN_NAME = 'aDomain' + 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 = '1' - TEST_TENANT_NAME = 'aTenant' - TEST_TOKEN = 'aToken' - TEST_TRUST_ID = 'aTrust' - TEST_USER = 'test' + 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.mox = mox.Mox() - self.logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) - self.time_patcher = mock.patch.object(time, 'time', lambda: 1234) - self.time_patcher.start() + self.deprecations = self.useFixture(client_fixtures.Deprecations()) + self.logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) self.requests_mock = self.useFixture(fixture.Fixture()) - def tearDown(self): - self.time_patcher.stop() - self.mox.UnsetStubs() - self.mox.VerifyAll() - super(TestCase, self).tearDown() - def stub_url(self, method, parts=None, base_url=None, json=None, **kwargs): if not base_url: base_url = self.TEST_URL @@ -82,7 +74,7 @@ def assertRequestBodyIs(self, body=None, json=None): self.assertEqual(body, last_request_body) def assertQueryStringIs(self, qs=''): - """Verify the QueryString matches what is expected. + r"""Verify the QueryString matches what is expected. The qs parameter should be of the format \'foo=bar&abc=xyz\' """ @@ -104,12 +96,12 @@ def assertQueryStringContains(self, **kwargs): parts = urlparse.urlparse(self.requests_mock.last_request.url) qs = urlparse.parse_qs(parts.query, keep_blank_values=True) - for k, v in six.iteritems(kwargs): + 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 + """Verify that the last request made contains a header and its value. The request must have already been made. """ @@ -117,49 +109,9 @@ def assertRequestHeaderEqual(self, name, val): self.assertEqual(headers.get(name), val) -if tuple(sys.version_info)[0:2] < (2, 7): - - def assertDictEqual(self, d1, d2, msg=None): - # Simple version taken from 2.7 - self.assertIsInstance(d1, dict, - 'First argument is not a dictionary') - self.assertIsInstance(d2, dict, - 'Second argument is not a dictionary') - if d1 != d2: - if msg: - self.fail(msg) - else: - standardMsg = '%r != %r' % (d1, d2) - self.fail(standardMsg) - - TestCase.assertDictEqual = assertDictEqual - - -class TestResponse(requests.Response): - """Class used to wrap requests.Response and provide some - convenience to initialize with a dict. - """ - - def __init__(self, data): - self._text = None - super(TestResponse, self).__init__() - if isinstance(data, dict): - self.status_code = data.get('status_code', 200) - headers = data.get('headers') - if headers: - self.headers.update(headers) - # Fake the text attribute to streamline Response creation - # _content is defined by requests.Response - self._content = data.get('text') - else: - self.status_code = data - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - - @property - def text(self): - return self.content +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): @@ -179,7 +131,7 @@ def tearDown(self): def clear_module(self): cleared_modules = {} - for fullname in sys.modules.keys(): + for fullname in list(sys.modules): if (fullname == self.module or fullname.startswith(self.module + '.')): cleared_modules[fullname] = sys.modules.pop(fullname) @@ -187,7 +139,6 @@ def clear_module(self): def setUp(self): """Ensure ImportError for the specified module.""" - super(DisableModuleFixture, self).setUp() # Clear 'module' references in sys.modules @@ -198,6 +149,30 @@ def setUp(self): 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'.""" diff --git a/keystoneclient/tests/unit/v2_0/client_fixtures.py b/keystoneclient/tests/unit/v2_0/client_fixtures.py index 0ce318d32..d3d8bcac3 100644 --- a/keystoneclient/tests/unit/v2_0/client_fixtures.py +++ b/keystoneclient/tests/unit/v2_0/client_fixtures.py @@ -9,11 +9,9 @@ # 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 __future__ import unicode_literals import uuid -from keystoneclient import fixture +from keystoneauth1 import fixture def unscoped_token(): diff --git a/keystoneclient/tests/unit/v2_0/test_access.py b/keystoneclient/tests/unit/v2_0/test_access.py index e966874dd..95556aff2 100644 --- a/keystoneclient/tests/unit/v2_0/test_access.py +++ b/keystoneclient/tests/unit/v2_0/test_access.py @@ -13,11 +13,11 @@ import datetime import uuid +from keystoneauth1 import fixture from oslo_utils import timeutils import testresources from keystoneclient import access -from keystoneclient import fixture 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 @@ -45,10 +45,13 @@ def test_building_unscoped_accessinfo(self): self.assertIsNone(auth_ref.tenant_name) self.assertIsNone(auth_ref.tenant_id) - self.assertIsNone(auth_ref.auth_url) - self.assertIsNone(auth_ref.management_url) + 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.scoped) + 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) @@ -98,15 +101,20 @@ def test_building_scoped_accessinfo(self): self.assertEqual(auth_ref.tenant_name, auth_ref.project_name) self.assertEqual(auth_ref.tenant_id, auth_ref.project_id) - self.assertEqual(auth_ref.auth_url, ('http://public.com:5000/v2.0',)) - self.assertEqual(auth_ref.management_url, ('http://admin:35357/v2.0',)) + 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') - self.assertTrue(auth_ref.scoped) + with self.deprecations.expect_deprecations_here(): + self.assertTrue(auth_ref.scoped) self.assertTrue(auth_ref.project_scoped) self.assertFalse(auth_ref.domain_scoped) @@ -127,7 +135,8 @@ def test_diablo_token(self): self.assertEqual(auth_ref.user_domain_id, 'default') self.assertEqual(auth_ref.user_domain_name, 'Default') self.assertEqual(auth_ref.role_names, ['role1', 'role2']) - self.assertFalse(auth_ref.scoped) + with self.deprecations.expect_deprecations_here(): + self.assertFalse(auth_ref.scoped) def test_grizzly_token(self): grizzly_token = self.examples.TOKEN_RESPONSES[ diff --git a/keystoneclient/tests/unit/v2_0/test_auth.py b/keystoneclient/tests/unit/v2_0/test_auth.py index 2c69dc3d1..b73352471 100644 --- a/keystoneclient/tests/unit/v2_0/test_auth.py +++ b/keystoneclient/tests/unit/v2_0/test_auth.py @@ -15,6 +15,7 @@ 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 @@ -27,7 +28,7 @@ def setUp(self): self.TEST_RESPONSE_DICT = { "access": { "token": { - "expires": "2020-01-01T00:00:10.000123Z", + "expires": "2999-01-01T00:00:10.000123Z", "id": self.TEST_TOKEN, "tenant": { "id": self.TEST_TENANT_ID @@ -60,23 +61,26 @@ def test_authenticate_success_expired(self): # Build a new response TEST_TOKEN = "abcdef" - resp_b['access']['token']['expires'] = '2020-01-01T00:00:10.000123Z' + 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}]) - cs = client.Client(tenant_id=self.TEST_TENANT_ID, - auth_url=self.TEST_URL, - username=self.TEST_USER, - password=self.TEST_TOKEN) + # 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"]) - self.assertEqual(cs.auth_token, TEST_TOKEN) + 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): @@ -89,16 +93,13 @@ def test_authenticate_failure(self): self.stub_auth(status_code=401, json=error) - # 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) + 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.assertRaises(exceptions.Unauthorized, client_create_wrapper) self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) def test_auth_redirect(self): @@ -108,10 +109,11 @@ def test_auth_redirect(self): self.stub_auth(base_url=self.TEST_ADMIN_URL, json=self.TEST_RESPONSE_DICT) - cs = client.Client(username=self.TEST_USER, - password=self.TEST_TOKEN, - tenant_id=self.TEST_TENANT_ID, - auth_url=self.TEST_URL) + 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] @@ -123,10 +125,11 @@ def test_auth_redirect(self): def test_authenticate_success_password_scoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) - cs = client.Client(username=self.TEST_USER, - password=self.TEST_TOKEN, - tenant_id=self.TEST_TENANT_ID, - auth_url=self.TEST_URL) + 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"]) @@ -140,12 +143,13 @@ def test_authenticate_success_password_unscoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) - cs = client.Client(username=self.TEST_USER, - password=self.TEST_TOKEN, - auth_url=self.TEST_URL) + 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.assertFalse('serviceCatalog' in cs.service_catalog.catalog) + self.assertNotIn('serviceCatalog', cs.service_catalog.catalog) self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) def test_auth_url_token_authentication(self): @@ -157,12 +161,14 @@ def test_auth_url_token_authentication(self): self.stub_url('GET', [fake_url], json=fake_resp, base_url=self.TEST_ADMIN_IDENTITY_ENDPOINT) - cl = client.Client(auth_url=self.TEST_URL, - token=fake_token) + 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) - resp, body = cl.get(fake_url) + 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') @@ -173,9 +179,10 @@ def test_authenticate_success_token_scoped(self): self.TEST_REQUEST_BODY['auth']['token'] = {'id': self.TEST_TOKEN} self.stub_auth(json=self.TEST_RESPONSE_DICT) - cs = client.Client(token=self.TEST_TOKEN, - tenant_id=self.TEST_TENANT_ID, - auth_url=self.TEST_URL) + 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"]) @@ -192,10 +199,11 @@ def test_authenticate_success_token_scoped_trust(self): "id": self.TEST_TRUST_ID} self.stub_auth(json=response) - cs = client.Client(token=self.TEST_TOKEN, - tenant_id=self.TEST_TENANT_ID, - trust_id=self.TEST_TRUST_ID, - auth_url=self.TEST_URL) + 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) @@ -209,11 +217,12 @@ def test_authenticate_success_token_unscoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) - cs = client.Client(token=self.TEST_TOKEN, - auth_url=self.TEST_URL) + 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.assertFalse('serviceCatalog' in cs.service_catalog.catalog) + self.assertNotIn('serviceCatalog', cs.service_catalog.catalog) self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) def test_allow_override_of_auth_token(self): @@ -225,15 +234,16 @@ def test_allow_override_of_auth_token(self): self.stub_url('GET', [fake_url], json=fake_resp, base_url=self.TEST_ADMIN_IDENTITY_ENDPOINT) - cl = client.Client(username='exampleuser', - password='password', - tenant_name='exampleproject', - auth_url=self.TEST_URL) + 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.get(fake_url) + resp, body = cl._adapter.get(fake_url) self.assertEqual(fake_resp, body) token = self.requests_mock.last_request.headers.get('X-Auth-Token') @@ -242,7 +252,7 @@ def test_allow_override_of_auth_token(self): # then override that token and the new token shall be used cl.auth_token = fake_token - resp, body = cl.get(fake_url) + resp, body = cl._adapter.get(fake_url) self.assertEqual(fake_resp, body) token = self.requests_mock.last_request.headers.get('X-Auth-Token') @@ -251,7 +261,7 @@ def test_allow_override_of_auth_token(self): # if we clear that overridden token then we fall back to the original del cl.auth_token - resp, body = cl.get(fake_url) + resp, body = cl._adapter.get(fake_url) self.assertEqual(fake_resp, body) token = self.requests_mock.last_request.headers.get('X-Auth-Token') diff --git a/keystoneclient/tests/unit/v2_0/test_certificates.py b/keystoneclient/tests/unit/v2_0/test_certificates.py index fc19d8119..4f689d9ad 100644 --- a/keystoneclient/tests/unit/v2_0/test_certificates.py +++ b/keystoneclient/tests/unit/v2_0/test_certificates.py @@ -17,7 +17,7 @@ from keystoneclient.tests.unit.v2_0 import utils -class CertificateTests(utils.TestCase, testresources.ResourcedTestCase): +class CertificateTests(utils.ClientTestCase, testresources.ResourcedTestCase): resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] diff --git a/keystoneclient/tests/unit/v2_0/test_client.py b/keystoneclient/tests/unit/v2_0/test_client.py index 379bea4e7..7fe9b1813 100644 --- a/keystoneclient/tests/unit/v2_0/test_client.py +++ b/keystoneclient/tests/unit/v2_0/test_client.py @@ -10,14 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. -import json import uuid -import six +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 fixture from keystoneclient import session from keystoneclient.tests.unit.v2_0 import client_fixtures from keystoneclient.tests.unit.v2_0 import utils @@ -30,11 +31,14 @@ def test_unscoped_init(self): token = client_fixtures.unscoped_token() self.stub_auth(json=token) - c = client.Client(username='exampleuser', - password='password', - auth_url=self.TEST_URL) + # 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) - self.assertFalse(c.auth_ref.scoped) + 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) @@ -46,12 +50,15 @@ def test_scoped_init(self): token = client_fixtures.project_scoped_token() self.stub_auth(json=token) - c = client.Client(username='exampleuser', - password='password', - tenant_name='exampleproject', - auth_url=self.TEST_URL) + # 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) - self.assertTrue(c.auth_ref.scoped) + 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) @@ -63,14 +70,19 @@ def test_scoped_init(self): def test_auth_ref_load(self): self.stub_auth(json=client_fixtures.project_scoped_token()) - cl = client.Client(username='exampleuser', - password='password', - tenant_name='exampleproject', - auth_url=self.TEST_URL) - cache = json.dumps(cl.auth_ref) - new_client = client.Client(auth_ref=json.loads(cache)) + # 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) - self.assertTrue(new_client.auth_ref.scoped) + 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) @@ -83,17 +95,21 @@ def test_auth_ref_load(self): def test_auth_ref_load_with_overridden_arguments(self): self.stub_auth(json=client_fixtures.project_scoped_token()) - cl = client.Client(username='exampleuser', - password='password', - tenant_name='exampleproject', - auth_url=self.TEST_URL) - cache = json.dumps(cl.auth_ref) + # 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" - new_client = client.Client(auth_ref=json.loads(cache), - auth_url=new_auth_url) + # 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.assertTrue(new_client.auth_ref.scoped) - self.assertTrue(new_client.auth_ref.scoped) + 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) @@ -105,10 +121,12 @@ def test_auth_ref_load_with_overridden_arguments(self): 'http://admin:35357/v2.0') def test_init_err_no_auth_url(self): - self.assertRaises(exceptions.AuthorizationFailure, - client.Client, - username='exampleuser', - password='password') + # 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() @@ -128,13 +146,16 @@ def test_management_url_is_updated(self): self.stub_auth(response_list=[{'json': first}, {'json': second}]) - cl = client.Client(username='exampleuser', - password='password', - tenant_name='exampleproject', - auth_url=self.TEST_URL) + # 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) - cl.authenticate() + 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): @@ -142,30 +163,40 @@ def test_client_with_region_name_passes_to_service_catalog(self): # removed ASAP, however must remain compatible. self.stub_auth(json=client_fixtures.auth_response_body()) - cl = client.Client(username='exampleuser', - password='password', - tenant_name='exampleproject', - auth_url=self.TEST_URL, - region_name='North') + # 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/') - cl = client.Client(username='exampleuser', - password='password', - tenant_name='exampleproject', - auth_url=self.TEST_URL, - region_name='South') + # 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): - self.assertRaises(exceptions.AuthorizationFailure, - client.Client, - tenant_name='exampleproject', - auth_url=self.TEST_URL) + # 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): - opts = {'auth': token_endpoint.Token('a', 'b'), + 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, @@ -174,11 +205,17 @@ def test_client_params(self): 'user_agent': uuid.uuid4().hex, } - sess = session.Session() cl = client.Client(session=sess, **opts) - for k, v in six.iteritems(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/keystoneclient/tests/unit/v2_0/test_discovery.py b/keystoneclient/tests/unit/v2_0/test_discovery.py index 348038a43..a3700e0e1 100644 --- a/keystoneclient/tests/unit/v2_0/test_discovery.py +++ b/keystoneclient/tests/unit/v2_0/test_discovery.py @@ -29,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", @@ -55,7 +55,9 @@ def test_get_versions(self): 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) @@ -69,7 +71,9 @@ def test_get_version_local(self): 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) diff --git a/keystoneclient/tests/unit/v2_0/test_ec2.py b/keystoneclient/tests/unit/v2_0/test_ec2.py index e08d228f2..3df053de1 100644 --- a/keystoneclient/tests/unit/v2_0/test_ec2.py +++ b/keystoneclient/tests/unit/v2_0/test_ec2.py @@ -14,7 +14,7 @@ from keystoneclient.v2_0 import ec2 -class EC2Tests(utils.TestCase): +class EC2Tests(utils.ClientTestCase): def test_create(self): user_id = 'usr' diff --git a/keystoneclient/tests/unit/v2_0/test_endpoints.py b/keystoneclient/tests/unit/v2_0/test_endpoints.py index ffc1c65d1..7b15cccc9 100644 --- a/keystoneclient/tests/unit/v2_0/test_endpoints.py +++ b/keystoneclient/tests/unit/v2_0/test_endpoints.py @@ -16,7 +16,7 @@ from keystoneclient.v2_0 import endpoints -class EndpointTests(utils.TestCase): +class EndpointTests(utils.ClientTestCase): def setUp(self): super(EndpointTests, self).setUp() self.TEST_ENDPOINTS = { diff --git a/keystoneclient/tests/unit/v2_0/test_extensions.py b/keystoneclient/tests/unit/v2_0/test_extensions.py index d09943f4b..3927bc07c 100644 --- a/keystoneclient/tests/unit/v2_0/test_extensions.py +++ b/keystoneclient/tests/unit/v2_0/test_extensions.py @@ -14,7 +14,7 @@ from keystoneclient.v2_0 import extensions -class ExtensionTests(utils.TestCase): +class ExtensionTests(utils.ClientTestCase): def setUp(self): super(ExtensionTests, self).setUp() self.TEST_EXTENSIONS = { @@ -22,7 +22,7 @@ def setUp(self): "values": [ { 'name': 'OpenStack Keystone User CRUD', - 'namespace': 'http://docs.openstack.org/' + 'namespace': 'https://docs.openstack.org/' 'identity/api/ext/OS-KSCRUD/v1.0', 'updated': '2013-07-07T12:00:0-00:00', 'alias': 'OS-KSCRUD', @@ -36,7 +36,7 @@ def setUp(self): }, { 'name': 'OpenStack EC2 API', - 'namespace': 'http://docs.openstack.org/' + 'namespace': 'https://docs.openstack.org/' 'identity/api/ext/OS-EC2/v1.0', 'updated': '2013-09-07T12:00:0-00:00', 'alias': 'OS-EC2', diff --git a/keystoneclient/tests/unit/v2_0/test_roles.py b/keystoneclient/tests/unit/v2_0/test_roles.py index 74ad1ed54..fab21383c 100644 --- a/keystoneclient/tests/unit/v2_0/test_roles.py +++ b/keystoneclient/tests/unit/v2_0/test_roles.py @@ -16,7 +16,7 @@ from keystoneclient.v2_0 import roles -class RoleTests(utils.TestCase): +class RoleTests(utils.ClientTestCase): def setUp(self): super(RoleTests, self).setUp() diff --git a/keystoneclient/tests/unit/v2_0/test_service_catalog.py b/keystoneclient/tests/unit/v2_0/test_service_catalog.py index fddda6de7..612b7f0a7 100644 --- a/keystoneclient/tests/unit/v2_0/test_service_catalog.py +++ b/keystoneclient/tests/unit/v2_0/test_service_catalog.py @@ -10,9 +10,10 @@ # 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 import fixture from keystoneclient.tests.unit.v2_0 import client_fixtures from keystoneclient.tests.unit.v2_0 import utils @@ -48,14 +49,18 @@ def test_service_catalog_endpoints(self): def test_service_catalog_regions(self): self.AUTH_RESPONSE_BODY['access']['region_name'] = "North" - auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + # 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" - auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + # 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') @@ -133,7 +138,9 @@ def test_servcie_catalog_get_url_region_names(self): def test_service_catalog_param_overrides_body_region(self): self.AUTH_RESPONSE_BODY['access']['region_name'] = "North" - auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + # 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') diff --git a/keystoneclient/tests/unit/v2_0/test_services.py b/keystoneclient/tests/unit/v2_0/test_services.py index 21727453e..72c547699 100644 --- a/keystoneclient/tests/unit/v2_0/test_services.py +++ b/keystoneclient/tests/unit/v2_0/test_services.py @@ -16,7 +16,7 @@ from keystoneclient.v2_0 import services -class ServiceTests(utils.TestCase): +class ServiceTests(utils.ClientTestCase): def setUp(self): super(ServiceTests, self).setUp() @@ -42,7 +42,7 @@ def setUp(self): }, } - def test_create(self): + def test_create_with_description(self): req_body = { "OS-KSADM:service": { "name": "swift", @@ -68,6 +68,37 @@ def test_create(self): 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): diff --git a/keystoneclient/tests/unit/v2_0/test_shell.py b/keystoneclient/tests/unit/v2_0/test_shell.py deleted file mode 100644 index 85bbca5f0..000000000 --- a/keystoneclient/tests/unit/v2_0/test_shell.py +++ /dev/null @@ -1,464 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os -import sys - -import mock -from oslo_serialization import jsonutils -import six -from testtools import matchers - -from keystoneclient import fixture -from keystoneclient.tests.unit.v2_0 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/' -DEFAULT_ADMIN_URL = 'http://127.0.0.1:35357/v2.0/' - - -class ShellTests(utils.TestCase): - - TEST_URL = DEFAULT_ADMIN_URL - - def setUp(self): - """Patch os.environ to avoid required auth info.""" - - super(ShellTests, self).setUp() - - self.old_environment = os.environ.copy() - os.environ = { - '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, - } - import keystoneclient.shell - self.shell = keystoneclient.shell.OpenStackIdentityShell() - - self.token = fixture.V2Token() - self.token.set_scope() - svc = self.token.add_service('identity') - svc.add_endpoint(public=DEFAULT_AUTH_URL, - admin=DEFAULT_ADMIN_URL) - - self.stub_auth(json=self.token, base_url=DEFAULT_AUTH_URL) - - def tearDown(self): - os.environ = self.old_environment - super(ShellTests, self).tearDown() - - def run_command(self, cmd): - orig = sys.stdout - try: - sys.stdout = six.StringIO() - if isinstance(cmd, list): - self.shell.main(cmd) - else: - self.shell.main(cmd.split()) - except SystemExit: - exc_type, exc_value, exc_traceback = sys.exc_info() - self.assertEqual(exc_value.code, 0) - finally: - out = sys.stdout.getvalue() - sys.stdout.close() - sys.stdout = orig - return out - - def assert_called(self, method, path, base_url=TEST_URL): - self.assertEqual(method, self.requests_mock.last_request.method) - self.assertEqual(base_url + path.lstrip('/'), - self.requests_mock.last_request.url) - - def test_user_list(self): - self.stub_url('GET', ['users'], json={'users': []}) - self.run_command('user-list') - self.assert_called('GET', '/users') - - def test_user_create(self): - self.stub_url('POST', ['users'], json={'user': {}}) - self.run_command('user-create --name new-user') - - self.assert_called('POST', '/users') - self.assertRequestBodyIs(json={'user': {'email': None, - 'password': None, - 'enabled': True, - 'name': 'new-user', - 'tenantId': None}}) - - @mock.patch('sys.stdin', autospec=True) - def test_user_create_password_prompt(self, mock_stdin): - self.stub_url('POST', ['users'], json={'user': {}}) - - with mock.patch('getpass.getpass') as mock_getpass: - del(os.environ['OS_PASSWORD']) - mock_stdin.isatty = lambda: True - mock_getpass.return_value = 'newpass' - self.run_command('user-create --name new-user --pass') - - self.assert_called('POST', '/users') - self.assertRequestBodyIs(json={'user': {'email': None, - 'password': 'newpass', - 'enabled': True, - 'name': 'new-user', - 'tenantId': None}}) - - def test_user_get(self): - self.stub_url('GET', ['users', '1'], - json={'user': {'id': '1'}}) - self.run_command('user-get 1') - self.assert_called('GET', '/users/1') - - def test_user_delete(self): - self.stub_url('GET', ['users', '1'], - json={'user': {'id': '1'}}) - self.stub_url('DELETE', ['users', '1']) - self.run_command('user-delete 1') - self.assert_called('DELETE', '/users/1') - - def test_user_password_update(self): - self.stub_url('GET', ['users', '1'], - json={'user': {'id': '1'}}) - self.stub_url('PUT', ['users', '1', 'OS-KSADM', 'password']) - self.run_command('user-password-update --pass newpass 1') - self.assert_called('PUT', '/users/1/OS-KSADM/password') - - def test_user_update(self): - self.stub_url('PUT', ['users', '1']) - self.stub_url('GET', ['users', '1'], - json={"user": {"tenantId": "1", - "enabled": "true", - "id": "1", - "name": "username"}}) - - self.run_command('user-update --name new-user1' - ' --email user@email.com --enabled true 1') - self.assert_called('PUT', '/users/1') - body = {'user': {'id': '1', 'email': 'user@email.com', - 'enabled': True, 'name': 'new-user1'}} - self.assertRequestBodyIs(json=body) - - required = 'User not updated, no arguments present.' - out = self.run_command('user-update 1') - self.assertThat(out, matchers.MatchesRegex(required)) - - self.run_command(['user-update', '--email', '', '1']) - self.assert_called('PUT', '/users/1') - self.assertRequestBodyIs(json={'user': {'id': '1', 'email': ''}}) - - def test_role_create(self): - self.stub_url('POST', ['OS-KSADM', 'roles'], json={'role': {}}) - self.run_command('role-create --name new-role') - self.assert_called('POST', '/OS-KSADM/roles') - self.assertRequestBodyIs(json={"role": {"name": "new-role"}}) - - def test_role_get(self): - self.stub_url('GET', ['OS-KSADM', 'roles', '1'], - json={'role': {'id': '1'}}) - self.run_command('role-get 1') - self.assert_called('GET', '/OS-KSADM/roles/1') - - def test_role_list(self): - self.stub_url('GET', ['OS-KSADM', 'roles'], json={'roles': []}) - self.run_command('role-list') - self.assert_called('GET', '/OS-KSADM/roles') - - def test_role_delete(self): - self.stub_url('GET', ['OS-KSADM', 'roles', '1'], - json={'role': {'id': '1'}}) - self.stub_url('DELETE', ['OS-KSADM', 'roles', '1']) - self.run_command('role-delete 1') - self.assert_called('DELETE', '/OS-KSADM/roles/1') - - def test_user_role_add(self): - self.stub_url('GET', ['users', '1'], - json={'user': {'id': '1'}}) - self.stub_url('GET', ['OS-KSADM', 'roles', '1'], - json={'role': {'id': '1'}}) - - self.stub_url('PUT', ['users', '1', 'roles', 'OS-KSADM', '1']) - self.run_command('user-role-add --user_id 1 --role_id 1') - self.assert_called('PUT', '/users/1/roles/OS-KSADM/1') - - def test_user_role_list(self): - self.stub_url('GET', ['tenants', self.token.tenant_id], - json={'tenant': {'id': self.token.tenant_id}}) - self.stub_url('GET', ['tenants', self.token.tenant_id, - 'users', self.token.user_id, 'roles'], - json={'roles': []}) - - url = '/tenants/%s/users/%s/roles' % (self.token.tenant_id, - self.token.user_id) - - self.run_command('user-role-list --user_id %s --tenant-id %s' % - (self.token.user_id, self.token.tenant_id)) - self.assert_called('GET', url) - - self.run_command('user-role-list --user_id %s' % self.token.user_id) - self.assert_called('GET', url) - - self.run_command('user-role-list') - self.assert_called('GET', url) - - def test_user_role_remove(self): - self.stub_url('GET', ['users', '1'], - json={'user': {'id': 1}}) - self.stub_url('GET', ['OS-KSADM', 'roles', '1'], - json={'role': {'id': 1}}) - self.stub_url('DELETE', - ['users', '1', 'roles', 'OS-KSADM', '1']) - - self.run_command('user-role-remove --user_id 1 --role_id 1') - self.assert_called('DELETE', '/users/1/roles/OS-KSADM/1') - - def test_tenant_create(self): - self.stub_url('POST', ['tenants'], json={'tenant': {}}) - self.run_command('tenant-create --name new-tenant') - self.assertRequestBodyIs(json={"tenant": {"enabled": True, - "name": "new-tenant", - "description": None}}) - - def test_tenant_get(self): - self.stub_url('GET', ['tenants', '2'], json={'tenant': {}}) - self.run_command('tenant-get 2') - self.assert_called('GET', '/tenants/2') - - def test_tenant_list(self): - self.stub_url('GET', ['tenants'], json={'tenants': []}) - self.run_command('tenant-list') - self.assert_called('GET', '/tenants') - - def test_tenant_update(self): - self.stub_url('GET', ['tenants', '1'], - json={'tenant': {'id': '1'}}) - self.stub_url('GET', ['tenants', '2'], - json={'tenant': {'id': '2'}}) - self.stub_url('POST', ['tenants', '2'], - json={'tenant': {'id': '2'}}) - self.run_command('tenant-update' - ' --name new-tenant1 --enabled false' - ' --description desc 2') - self.assert_called('POST', '/tenants/2') - self.assertRequestBodyIs(json={"tenant": {"enabled": False, - "id": "2", - "description": "desc", - "name": "new-tenant1"}}) - - required = 'Tenant not updated, no arguments present.' - out = self.run_command('tenant-update 1') - self.assertThat(out, matchers.MatchesRegex(required)) - - def test_tenant_delete(self): - self.stub_url('GET', ['tenants', '2'], - json={'tenant': {'id': '2'}}) - self.stub_url('DELETE', ['tenants', '2']) - self.run_command('tenant-delete 2') - self.assert_called('DELETE', '/tenants/2') - - def test_service_create_with_required_arguments_only(self): - self.stub_url('POST', ['OS-KSADM', 'services'], - json={'OS-KSADM:service': {}}) - self.run_command('service-create --type compute') - self.assert_called('POST', '/OS-KSADM/services') - json = {"OS-KSADM:service": {"type": "compute", - "name": None, - "description": None}} - self.assertRequestBodyIs(json=json) - - def test_service_create_with_all_arguments(self): - self.stub_url('POST', ['OS-KSADM', 'services'], - json={'OS-KSADM:service': {}}) - self.run_command('service-create --type compute ' - '--name service1 --description desc1') - self.assert_called('POST', '/OS-KSADM/services') - json = {"OS-KSADM:service": {"type": "compute", - "name": "service1", - "description": "desc1"}} - self.assertRequestBodyIs(json=json) - - def test_service_get(self): - self.stub_url('GET', ['OS-KSADM', 'services', '1'], - json={'OS-KSADM:service': {'id': '1'}}) - self.run_command('service-get 1') - self.assert_called('GET', '/OS-KSADM/services/1') - - def test_service_list(self): - self.stub_url('GET', ['OS-KSADM', 'services'], - json={'OS-KSADM:services': []}) - self.run_command('service-list') - self.assert_called('GET', '/OS-KSADM/services') - - def test_service_delete(self): - self.stub_url('GET', ['OS-KSADM', 'services', '1'], - json={'OS-KSADM:service': {'id': 1}}) - self.stub_url('DELETE', ['OS-KSADM', 'services', '1']) - self.run_command('service-delete 1') - self.assert_called('DELETE', '/OS-KSADM/services/1') - - def test_catalog(self): - self.run_command('catalog') - self.run_command('catalog --service compute') - - def test_ec2_credentials_create(self): - self.stub_url('POST', - ['users', self.token.user_id, 'credentials', 'OS-EC2'], - json={'credential': {}}) - - url = '/users/%s/credentials/OS-EC2' % self.token.user_id - self.run_command('ec2-credentials-create --tenant-id 1 ' - '--user-id %s' % self.token.user_id) - self.assert_called('POST', url) - self.assertRequestBodyIs(json={'tenant_id': '1'}) - - self.run_command('ec2-credentials-create --tenant-id 1') - self.assert_called('POST', url) - self.assertRequestBodyIs(json={'tenant_id': '1'}) - - self.run_command('ec2-credentials-create') - self.assert_called('POST', url) - self.assertRequestBodyIs(json={'tenant_id': self.token.tenant_id}) - - def test_ec2_credentials_delete(self): - self.stub_url('DELETE', - ['users', self.token.user_id, - 'credentials', 'OS-EC2', '2']) - self.run_command('ec2-credentials-delete --access 2 --user-id %s' % - self.token.user_id) - - url = '/users/%s/credentials/OS-EC2/2' % self.token.user_id - self.assert_called('DELETE', url) - - self.run_command('ec2-credentials-delete --access 2') - self.assert_called('DELETE', url) - - def test_ec2_credentials_list(self): - self.stub_url('GET', - ['users', self.token.user_id, 'credentials', 'OS-EC2'], - json={'credentials': []}) - self.run_command('ec2-credentials-list --user-id %s' - % self.token.user_id) - - url = '/users/%s/credentials/OS-EC2' % self.token.user_id - self.assert_called('GET', url) - - self.run_command('ec2-credentials-list') - self.assert_called('GET', url) - - def test_ec2_credentials_get(self): - self.stub_url('GET', - ['users', '1', 'credentials', 'OS-EC2', '2'], - json={'credential': {}}) - self.run_command('ec2-credentials-get --access 2 --user-id 1') - self.assert_called('GET', '/users/1/credentials/OS-EC2/2') - - def test_bootstrap(self): - user = {'user': {'id': '1'}} - role = {'role': {'id': '1'}} - tenant = {'tenant': {'id': '1'}} - - token = fixture.V2Token(user_id=1, tenant_id=1) - token.add_role(id=1) - svc = token.add_service('identity') - svc.add_endpoint(public=DEFAULT_AUTH_URL, - admin=DEFAULT_ADMIN_URL) - - self.stub_auth(json=token) - - self.stub_url('POST', ['OS-KSADM', 'roles'], json=role) - self.stub_url('GET', ['OS-KSADM', 'roles', '1'], json=role) - self.stub_url('POST', ['tenants'], json=tenant) - self.stub_url('GET', ['tenants', '1'], json=tenant) - self.stub_url('POST', ['users'], json=user) - self.stub_url('GET', ['users', '1'], json=user) - self.stub_url('PUT', - ['tenants', '1', 'users', '1', 'roles', 'OS-KSADM', '1'], - json=role) - - self.run_command('bootstrap --user-name new-user' - ' --pass 1 --role-name admin' - ' --tenant-name new-tenant') - - def called_anytime(method, path, json=None): - test_url = self.TEST_URL.strip('/') - for r in self.requests_mock.request_history: - if not r.method == method: - continue - if not r.url == test_url + path: - continue - - if json: - json_body = jsonutils.loads(r.body) - if not json_body == json: - continue - - return True - - raise AssertionError('URL never called') - - called_anytime('POST', '/users', {'user': {'email': None, - 'password': '1', - 'enabled': True, - 'name': 'new-user', - 'tenantId': None}}) - - called_anytime('POST', '/tenants', {"tenant": {"enabled": True, - "name": "new-tenant", - "description": None}}) - - called_anytime('POST', '/OS-KSADM/roles', - {"role": {"name": "admin"}}) - - called_anytime('PUT', '/tenants/1/users/1/roles/OS-KSADM/1') - - def test_bash_completion(self): - self.run_command('bash-completion') - - def test_help(self): - out = self.run_command('help') - required = 'usage: keystone' - self.assertThat(out, matchers.MatchesRegex(required)) - - def test_password_update(self): - self.stub_url('PATCH', - ['OS-KSCRUD', 'users', self.token.user_id], - base_url=DEFAULT_AUTH_URL) - self.run_command('password-update --current-password oldpass' - ' --new-password newpass') - self.assert_called('PATCH', - '/OS-KSCRUD/users/%s' % self.token.user_id, - base_url=DEFAULT_AUTH_URL) - self.assertRequestBodyIs(json={'user': {'original_password': 'oldpass', - 'password': 'newpass'}}) - - def test_endpoint_create(self): - self.stub_url('GET', ['OS-KSADM', 'services', '1'], - json={'OS-KSADM:service': {'id': '1'}}) - self.stub_url('POST', ['endpoints'], json={'endpoint': {}}) - self.run_command('endpoint-create --service-id 1 ' - '--publicurl=http://example.com:1234/go') - self.assert_called('POST', '/endpoints') - json = {'endpoint': {'adminurl': None, - 'service_id': '1', - 'region': 'regionOne', - 'internalurl': None, - 'publicurl': "http://example.com:1234/go"}} - self.assertRequestBodyIs(json=json) - - def test_endpoint_list(self): - self.stub_url('GET', ['endpoints'], json={'endpoints': []}) - self.run_command('endpoint-list') - self.assert_called('GET', '/endpoints') diff --git a/keystoneclient/tests/unit/v2_0/test_tenants.py b/keystoneclient/tests/unit/v2_0/test_tenants.py index 62ca39899..5e78aa3be 100644 --- a/keystoneclient/tests/unit/v2_0/test_tenants.py +++ b/keystoneclient/tests/unit/v2_0/test_tenants.py @@ -12,15 +12,16 @@ import uuid -from keystoneclient import exceptions -from keystoneclient import fixture +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.TestCase): +class TenantTests(utils.ClientTestCase): def setUp(self): super(TenantTests, self).setUp() @@ -332,9 +333,9 @@ def test_tenant_list_users(self): def test_list_tenants_use_admin_url(self): self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS) - self.assertEqual(self.TEST_URL, self.client.management_url) - 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']), @@ -351,9 +352,11 @@ def test_list_tenants_fallback_to_auth_url(self): self.stub_url('GET', ['tenants'], base_url=new_auth_url, json=self.TEST_TENANTS) - c = client.Client(username=self.TEST_USER, - auth_url=new_auth_url, - password=uuid.uuid4().hex) + # 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() diff --git a/keystoneclient/tests/unit/v2_0/test_tokens.py b/keystoneclient/tests/unit/v2_0/test_tokens.py index 8a40f8295..58df7c219 100644 --- a/keystoneclient/tests/unit/v2_0/test_tokens.py +++ b/keystoneclient/tests/unit/v2_0/test_tokens.py @@ -12,15 +12,16 @@ import uuid +from keystoneauth1 import exceptions +from keystoneauth1 import fixture + from keystoneclient import access -from keystoneclient import exceptions -from keystoneclient import fixture from keystoneclient.tests.unit.v2_0 import utils from keystoneclient.v2_0 import client from keystoneclient.v2_0 import tokens -class TokenTests(utils.TestCase): +class TokenTests(utils.ClientTestCase): def test_delete(self): id_ = uuid.uuid4().hex @@ -139,9 +140,9 @@ def test_authenticate_use_admin_url(self): token_fixture.set_scope() self.stub_auth(json=token_fixture) - self.assertEqual(self.TEST_URL, self.client.management_url) - 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) @@ -152,9 +153,11 @@ def test_authenticate_fallback_to_auth_url(self): token_fixture = fixture.V2Token() self.stub_auth(base_url=new_auth_url, json=token_fixture) - c = client.Client(username=self.TEST_USER, - auth_url=new_auth_url, - password=uuid.uuid4().hex) + # 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) @@ -168,6 +171,9 @@ def test_validate_token(self): 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) @@ -178,6 +184,9 @@ def test_validate_token_invalid_token(self): 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_) diff --git a/keystoneclient/tests/unit/v2_0/test_users.py b/keystoneclient/tests/unit/v2_0/test_users.py index 455ca3cf5..d65073887 100644 --- a/keystoneclient/tests/unit/v2_0/test_users.py +++ b/keystoneclient/tests/unit/v2_0/test_users.py @@ -17,7 +17,7 @@ from keystoneclient.v2_0 import users -class UserTests(utils.TestCase): +class UserTests(utils.ClientTestCase): def setUp(self): super(UserTests, self).setUp() self.ADMIN_USER_ID = uuid.uuid4().hex @@ -191,7 +191,6 @@ def test_list_limit_marker(self): def test_update(self): req_1 = { "user": { - "id": self.DEMO_USER_ID, "email": "gabriel@example.com", "name": "gabriel", } @@ -199,20 +198,17 @@ def test_update(self): password = uuid.uuid4().hex req_2 = { "user": { - "id": self.DEMO_USER_ID, "password": password, } } tenant_id = uuid.uuid4().hex req_3 = { "user": { - "id": self.DEMO_USER_ID, "tenantId": tenant_id, } } req_4 = { "user": { - "id": self.DEMO_USER_ID, "enabled": False, } } @@ -252,10 +248,10 @@ def test_update_own_password(self): resp_body = { 'access': {} } - user_id = uuid.uuid4().hex - self.stub_url('PATCH', ['OS-KSCRUD', 'users', user_id], json=resp_body) + self.stub_url('PATCH', + ['OS-KSCRUD', 'users', self.TEST_USER_ID], + json=resp_body) - self.client.user_id = user_id self.client.users.update_own_password(old_password, new_password) self.assertRequestBodyIs(json=req_body) self.assertNotIn(old_password, self.logger.output) diff --git a/keystoneclient/tests/unit/v2_0/utils.py b/keystoneclient/tests/unit/v2_0/utils.py index 475181f6f..6532d71b5 100644 --- a/keystoneclient/tests/unit/v2_0/utils.py +++ b/keystoneclient/tests/unit/v2_0/utils.py @@ -10,10 +10,8 @@ # 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 -from keystoneclient.v2_0 import client - -TestResponse = utils.TestResponse class UnauthenticatedTestCase(utils.TestCase): @@ -76,13 +74,17 @@ class TestCase(UnauthenticatedTestCase): "name": "swift" }] - def setUp(self): - super(TestCase, self).setUp() - 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 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/client_fixtures.py b/keystoneclient/tests/unit/v3/client_fixtures.py index 99e49f03d..e22e7da1c 100644 --- a/keystoneclient/tests/unit/v3/client_fixtures.py +++ b/keystoneclient/tests/unit/v3/client_fixtures.py @@ -9,33 +9,23 @@ # 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 __future__ import unicode_literals import uuid -from keystoneclient import fixture +from keystoneauth1 import fixture -def unscoped_token(): - return fixture.V3Token(user_id='c4da488862bd435c9e6c0275a0d0e49a', - user_name='exampleuser', - user_domain_id='4e6893b7ba0b4006840c3845660b86ed', - user_domain_name='exampledomain', - expires='2010-11-01T03:32:15-05:00') +def unscoped_token(**kwargs): + return fixture.V3Token(**kwargs) -def domain_scoped_token(): - f = fixture.V3Token(user_id='c4da488862bd435c9e6c0275a0d0e49a', - user_name='exampleuser', - user_domain_id='4e6893b7ba0b4006840c3845660b86ed', - user_domain_name='exampledomain', - expires='2010-11-01T03:32:15-05:00', - domain_id='8e9283b7ba0b1038840c3842058b86ab', - domain_name='anotherdomain', - audit_chain_id=uuid.uuid4().hex) +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(id='76e72a', name='admin') - f.add_role(id='f4f392', name='member') + f.add_role(name='admin') + f.add_role(name='member') region = 'RegionOne' s = f.add_service('volume') @@ -71,20 +61,15 @@ def domain_scoped_token(): return f -def project_scoped_token(): - f = fixture.V3Token(user_id='c4da488862bd435c9e6c0275a0d0e49a', - user_name='exampleuser', - user_domain_id='4e6893b7ba0b4006840c3845660b86ed', - user_domain_name='exampledomain', - expires='2010-11-01T03:32:15-05:00', - project_id='225da22d3ce34b15877ea70b2a575f58', - project_name='exampleproject', - project_domain_id='4e6893b7ba0b4006840c3845660b86ed', - project_domain_name='exampledomain', - audit_chain_id=uuid.uuid4().hex) +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(id='76e72a', name='admin') - f.add_role(id='f4f392', name='member') + f.add_role(name='admin') + f.add_role(name='member') region = 'RegionOne' tenant = '225da22d3ce34b15877ea70b2a575f58' @@ -122,7 +107,7 @@ def project_scoped_token(): return f -AUTH_SUBJECT_TOKEN = '3e2813b7ba0b4006840c3825860b86ed' +AUTH_SUBJECT_TOKEN = uuid.uuid4().hex AUTH_RESPONSE_HEADERS = { 'X-Subject-Token': AUTH_SUBJECT_TOKEN, @@ -130,19 +115,11 @@ def project_scoped_token(): def auth_response_body(): - f = fixture.V3Token(user_id='567', - user_name='test', - user_domain_id='1', - user_domain_name='aDomain', - expires='2010-11-01T03:32:15-05:00', - project_domain_id='123', - project_domain_name='aDomain', - project_id='345', - project_name='aTenant', - audit_chain_id=uuid.uuid4().hex) - - f.add_role(id='76e72a', name='admin') - f.add_role(id='f4f392', name='member') + 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( @@ -175,13 +152,6 @@ def auth_response_body(): def trust_token(): - return fixture.V3Token(user_id='0ca8f6', - user_name='exampleuser', - user_domain_id='4e6893b7ba0b4006840c3845660b86ed', - user_domain_name='exampledomain', - expires='2010-11-01T03:32:15-05:00', - trust_id='fe0aef', - trust_impersonation=False, - trustee_user_id='0ca8f6', - trustor_user_id='bd263c', - audit_chain_id=uuid.uuid4().hex) + f = fixture.V3Token(audit_chain_id=uuid.uuid4().hex) + f.set_trust_scope() + return f diff --git a/keystoneclient/tests/unit/v3/saml2_fixtures.py b/keystoneclient/tests/unit/v3/saml2_fixtures.py index f428a87ac..17c1395a8 100644 --- a/keystoneclient/tests/unit/v3/saml2_fixtures.py +++ b/keystoneclient/tests/unit/v3/saml2_fixtures.py @@ -10,9 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import six - -SP_SOAP_RESPONSE = six.b(""" -""") +""" -SAML2_ASSERTION = six.b(""" +SAML2_ASSERTION = b""" VALUE= -""") +""" UNSCOPED_TOKEN_HEADER = 'UNSCOPED_TOKEN' @@ -145,7 +143,7 @@ } ], "links": { - "self": "http://identity:35357/v3/OS-FEDERATION/projects", + "self": "http://identity:35357/v3/auth/projects", "previous": 'null', "next": 'null' } @@ -164,7 +162,7 @@ } ], "links": { - "self": "http://identity:35357/v3/OS-FEDERATION/domains", + "self": "http://identity:35357/v3/auth/domains", "previous": 'null', "next": 'null' } diff --git a/keystoneclient/tests/unit/v3/test_access.py b/keystoneclient/tests/unit/v3/test_access.py index f069f7162..26ca8f15e 100644 --- a/keystoneclient/tests/unit/v3/test_access.py +++ b/keystoneclient/tests/unit/v3/test_access.py @@ -13,17 +13,18 @@ import datetime import uuid +from keystoneauth1 import fixture from oslo_utils import timeutils from keystoneclient import access -from keystoneclient import fixture +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 = utils.TestResponse({ - "headers": client_fixtures.AUTH_RESPONSE_HEADERS -}) +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() @@ -38,10 +39,10 @@ def test_building_unscoped_accessinfo(self): self.assertIn('methods', auth_ref) self.assertNotIn('catalog', auth_ref) - self.assertEqual(auth_ref.auth_token, - '3e2813b7ba0b4006840c3825860b86ed') - self.assertEqual(auth_ref.username, 'exampleuser') - self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + 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, []) @@ -49,15 +50,18 @@ def test_building_unscoped_accessinfo(self): self.assertIsNone(auth_ref.project_name) self.assertIsNone(auth_ref.project_id) - self.assertIsNone(auth_ref.auth_url) - self.assertIsNone(auth_ref.management_url) + 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(auth_ref.user_domain_id, - '4e6893b7ba0b4006840c3845660b86ed') - self.assertEqual(auth_ref.user_domain_name, 'exampledomain') + 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) @@ -92,24 +96,24 @@ def test_building_domain_scoped_accessinfo(self): self.assertIn('catalog', auth_ref) self.assertTrue(auth_ref['catalog']) - self.assertEqual(auth_ref.auth_token, - '3e2813b7ba0b4006840c3825860b86ed') - self.assertEqual(auth_ref.username, 'exampleuser') - self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + 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(auth_ref.role_ids, ['76e72a', 'f4f392']) - self.assertEqual(auth_ref.role_names, ['admin', 'member']) + self.assertEqual(DOMAIN_SCOPED_TOKEN.role_ids, auth_ref.role_ids) + self.assertEqual(DOMAIN_SCOPED_TOKEN.role_names, auth_ref.role_names) - self.assertEqual(auth_ref.domain_name, 'anotherdomain') - self.assertEqual(auth_ref.domain_id, - '8e9283b7ba0b1038840c3842058b86ab') + 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(auth_ref.user_domain_id, - '4e6893b7ba0b4006840c3845660b86ed') - self.assertEqual(auth_ref.user_domain_name, 'exampledomain') + 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) @@ -130,36 +134,40 @@ def test_building_project_scoped_accessinfo(self): self.assertIn('catalog', auth_ref) self.assertTrue(auth_ref['catalog']) - self.assertEqual(auth_ref.auth_token, - '3e2813b7ba0b4006840c3825860b86ed') - self.assertEqual(auth_ref.username, 'exampleuser') - self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + 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(auth_ref.role_ids, ['76e72a', 'f4f392']) - self.assertEqual(auth_ref.role_names, ['admin', 'member']) + 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(auth_ref.project_name, 'exampleproject') - self.assertEqual(auth_ref.project_id, - '225da22d3ce34b15877ea70b2a575f58') + 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) - self.assertEqual(auth_ref.auth_url, - ('http://public.com:5000/v3',)) - self.assertEqual(auth_ref.management_url, - ('http://admin:35357/v3',)) - - self.assertEqual(auth_ref.project_domain_id, - '4e6893b7ba0b4006840c3845660b86ed') - self.assertEqual(auth_ref.project_domain_name, 'exampledomain') - - self.assertEqual(auth_ref.user_domain_id, - '4e6893b7ba0b4006840c3845660b86ed') - self.assertEqual(auth_ref.user_domain_name, 'exampledomain') + 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) 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 index b3f29d66f..d3c44adc7 100644 --- a/keystoneclient/tests/unit/v3/test_auth.py +++ b/keystoneclient/tests/unit/v3/test_auth.py @@ -11,6 +11,7 @@ # under the License. from oslo_serialization import jsonutils +from testtools import testcase from keystoneclient import exceptions from keystoneclient.tests.unit.v3 import utils @@ -27,7 +28,7 @@ def setUp(self): "password" ], - "expires_at": "2020-01-01T00:00:10.000123Z", + "expires_at": "2999-01-01T00:00:10.000123Z", "project": { "domain": { "id": self.TEST_DOMAIN_ID, @@ -86,10 +87,12 @@ def test_authenticate_success(self): self.stub_auth(json=self.TEST_RESPONSE_DICT, subject_token=TEST_TOKEN) - cs = client.Client(user_id=self.TEST_USER, - password=self.TEST_TOKEN, - project_id=self.TEST_TENANT_ID, - auth_url=self.TEST_URL) + # 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) @@ -101,17 +104,14 @@ def test_authenticate_failure(self): self.stub_auth(status_code=401, json=error) - # Workaround for issue with assertRaises on python2.6 - # where with assertRaises(exceptions.Unauthorized): doesn't work - # right - def client_create_wrapper(): - 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.assertRaises(exceptions.Unauthorized, client_create_wrapper) + 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): @@ -121,11 +121,13 @@ def test_auth_redirect(self): self.stub_auth(json=self.TEST_RESPONSE_DICT, base_url=self.TEST_ADMIN_URL) - 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) + # 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] @@ -136,11 +138,13 @@ def test_auth_redirect(self): def test_authenticate_success_domain_username_password_scoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) - 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) + # 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"]) @@ -166,10 +170,12 @@ def test_authenticate_success_userid_password_domain_scoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) - cs = client.Client(user_id=self.TEST_USER, - password=self.TEST_TOKEN, - domain_id=self.TEST_DOMAIN_ID, - auth_url=self.TEST_URL) + # 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, @@ -187,10 +193,12 @@ def test_authenticate_success_userid_password_project_scoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) - cs = client.Client(user_id=self.TEST_USER, - password=self.TEST_TOKEN, - project_id=self.TEST_TENANT_ID, - auth_url=self.TEST_URL) + # 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, @@ -206,13 +214,15 @@ def test_authenticate_success_password_unscoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) - cs = client.Client(user_domain_name=self.TEST_DOMAIN_NAME, - username=self.TEST_USER, - password=self.TEST_TOKEN, - auth_url=self.TEST_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, + auth_url=self.TEST_URL) self.assertEqual(cs.auth_token, self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) - self.assertFalse('catalog' in cs.service_catalog.catalog) + self.assertNotIn('catalog', cs.service_catalog.catalog) self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) def test_auth_url_token_authentication(self): @@ -222,14 +232,16 @@ def test_auth_url_token_authentication(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) self.stub_url('GET', [fake_url], json=fake_resp, - base_url=self.TEST_ADMIN_IDENTITY_ENDPOINT) + base_url=self.TEST_PUBLIC_IDENTITY_ENDPOINT) - cl = client.Client(auth_url=self.TEST_URL, - token=fake_token) + # 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.get(fake_url) + resp, body = cl._adapter.get(fake_url) self.assertEqual(fake_resp, body) token = self.requests_mock.last_request.headers.get('X-Auth-Token') @@ -257,9 +269,11 @@ def test_authenticate_success_token_domain_scoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) - cs = client.Client(token=self.TEST_TOKEN, - domain_id=self.TEST_DOMAIN_ID, - auth_url=self.TEST_URL) + # 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, @@ -279,9 +293,11 @@ def test_authenticate_success_token_project_scoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) - cs = client.Client(token=self.TEST_TOKEN, - project_id=self.TEST_TENANT_ID, - auth_url=self.TEST_URL) + # 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, @@ -303,11 +319,13 @@ def test_authenticate_success_token_unscoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) - cs = client.Client(token=self.TEST_TOKEN, - auth_url=self.TEST_URL) + # 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.assertFalse('catalog' in cs.service_catalog.catalog) + self.assertNotIn('catalog', cs.service_catalog.catalog) self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) def test_allow_override_of_auth_token(self): @@ -317,17 +335,19 @@ def test_allow_override_of_auth_token(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) self.stub_url('GET', [fake_url], json=fake_resp, - base_url=self.TEST_ADMIN_IDENTITY_ENDPOINT) + base_url=self.TEST_PUBLIC_IDENTITY_ENDPOINT) - cl = client.Client(username='exampleuser', - password='password', - tenant_name='exampleproject', - auth_url=self.TEST_URL) + # 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.get(fake_url) + resp, body = cl._adapter.get(fake_url) self.assertEqual(fake_resp, body) token = self.requests_mock.last_request.headers.get('X-Auth-Token') @@ -336,7 +356,7 @@ def test_allow_override_of_auth_token(self): # then override that token and the new token shall be used cl.auth_token = fake_token - resp, body = cl.get(fake_url) + resp, body = cl._adapter.get(fake_url) self.assertEqual(fake_resp, body) token = self.requests_mock.last_request.headers.get('X-Auth-Token') @@ -345,7 +365,7 @@ def test_allow_override_of_auth_token(self): # if we clear that overridden token then we fall back to the original del cl.auth_token - resp, body = cl.get(fake_url) + resp, body = cl._adapter.get(fake_url) self.assertEqual(fake_resp, body) token = self.requests_mock.last_request.headers.get('X-Auth-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 index d64d9624a..8c2f67daa 100644 --- a/keystoneclient/tests/unit/v3/test_auth_saml2.py +++ b/keystoneclient/tests/unit/v3/test_auth_saml2.py @@ -11,12 +11,12 @@ # under the License. import os +import urllib.parse import uuid from lxml import etree from oslo_config import fixture as config import requests -from six.moves import urllib from keystoneclient.auth import conf from keystoneclient.contrib.auth.v3 import saml2 @@ -63,6 +63,8 @@ class _AuthentiatedResponseMissingTokenID(_AuthenticatedResponse): 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) @@ -104,28 +106,7 @@ def setUp(self): self.TEST_USER, self.TEST_TOKEN) def test_conf_params(self): - section = uuid.uuid4().hex - identity_provider = uuid.uuid4().hex - identity_provider_url = uuid.uuid4().hex - username = uuid.uuid4().hex - password = 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(saml2.Saml2UnscopedToken.get_options(), - group=section) - self.conf_fixture.config(auth_plugin='v3unscopedsaml', - identity_provider=identity_provider, - identity_provider_url=identity_provider_url, - username=username, - password=password, - group=section) - - a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) - self.assertEqual(identity_provider, a.identity_provider) - self.assertEqual(identity_provider_url, a.identity_provider_url) - self.assertEqual(username, a.username) - self.assertEqual(password, a.password) + pass def test_initial_sp_call(self): """Test initial call, expect SOAP message.""" @@ -283,7 +264,32 @@ def test_custom_302_redirection(self): self.assertEqual(self.FEDERATION_AUTH_URL, response.headers['location']) - response = self.saml2plugin._handle_http_302_ecp_redirect( + 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) @@ -413,8 +419,11 @@ def _uuid4(self): 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' @@ -435,35 +444,10 @@ def setUp(self): self.ADFS_FAULT = _load_xml('ADFS_fault.xml') def test_conf_params(self): - section = uuid.uuid4().hex - identity_provider = uuid.uuid4().hex - identity_provider_url = uuid.uuid4().hex - sp_endpoint = uuid.uuid4().hex - username = uuid.uuid4().hex - password = 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(saml2.ADFSUnscopedToken.get_options(), - group=section) - self.conf_fixture.config(auth_plugin='v3unscopedadfs', - identity_provider=identity_provider, - identity_provider_url=identity_provider_url, - service_provider_endpoint=sp_endpoint, - username=username, - password=password, - group=section) - - a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) - self.assertEqual(identity_provider, a.identity_provider) - self.assertEqual(identity_provider_url, a.identity_provider_url) - self.assertEqual(sp_endpoint, a.service_provider_endpoint) - self.assertEqual(username, a.username) - self.assertEqual(password, a.password) + 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), @@ -630,7 +614,7 @@ def test_end_to_end_workflow(self): self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], token_json) -class SAMLGenerationTests(utils.TestCase): +class SAMLGenerationTests(utils.ClientTestCase): def setUp(self): super(SAMLGenerationTests, self).setUp() @@ -642,7 +626,6 @@ def setUp(self): 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 @@ -665,7 +648,6 @@ def test_saml_create(self): 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 diff --git a/keystoneclient/tests/unit/v3/test_client.py b/keystoneclient/tests/unit/v3/test_client.py index c01cac20d..82088fdfc 100644 --- a/keystoneclient/tests/unit/v3/test_client.py +++ b/keystoneclient/tests/unit/v3/test_client.py @@ -11,11 +11,11 @@ # under the License. import copy -import json import uuid -import six +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 @@ -27,71 +27,77 @@ class KeystoneClientTest(utils.TestCase): def test_unscoped_init(self): - self.stub_auth(json=client_fixtures.unscoped_token()) - - c = client.Client(user_domain_name='exampledomain', - username='exampleuser', - password='password', - auth_url=self.TEST_URL) + 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(c.auth_user_id, - 'c4da488862bd435c9e6c0275a0d0e49a') + self.assertEqual(token.user_id, c.auth_user_id) self.assertFalse(c.has_service_catalog()) - self.assertEqual('c4da488862bd435c9e6c0275a0d0e49a', - c.get_user_id(session=None)) + 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): - self.stub_auth(json=client_fixtures.domain_scoped_token()) - - c = client.Client(user_id='c4da488862bd435c9e6c0275a0d0e49a', - password='password', - domain_name='exampledomain', - auth_url=self.TEST_URL) + 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(c.auth_user_id, - 'c4da488862bd435c9e6c0275a0d0e49a') - self.assertEqual(c.auth_domain_id, - '8e9283b7ba0b1038840c3842058b86ab') + self.assertEqual(token.user_id, c.auth_user_id) + self.assertEqual(token.domain_id, c.auth_domain_id) def test_project_scoped_init(self): - self.stub_auth(json=client_fixtures.project_scoped_token()), - - c = client.Client(user_id='c4da488862bd435c9e6c0275a0d0e49a', - password='password', - user_domain_name='exampledomain', - project_name='exampleproject', - auth_url=self.TEST_URL) + 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(c.auth_user_id, - 'c4da488862bd435c9e6c0275a0d0e49a') - self.assertEqual(c.auth_tenant_id, - '225da22d3ce34b15877ea70b2a575f58') - self.assertEqual('c4da488862bd435c9e6c0275a0d0e49a', - c.get_user_id(session=None)) - self.assertEqual('225da22d3ce34b15877ea70b2a575f58', - c.get_project_id(session=None)) + 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): - self.stub_auth(json=client_fixtures.project_scoped_token()) - - c = client.Client(user_id='c4da488862bd435c9e6c0275a0d0e49a', - password='password', - project_id='225da22d3ce34b15877ea70b2a575f58', - auth_url=self.TEST_URL) - cache = json.dumps(c.auth_ref) - new_client = client.Client(auth_ref=json.loads(cache)) + 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(new_client.username, 'exampleuser') + self.assertEqual(token.user_name, new_client.username) self.assertIsNone(new_client.password) self.assertEqual(new_client.management_url, 'http://admin:35357/v3') @@ -99,48 +105,66 @@ def test_auth_ref_load(self): def test_auth_ref_load_with_overridden_arguments(self): new_auth_url = 'https://newkeystone.com/v3' - self.stub_auth(json=client_fixtures.project_scoped_token()) - self.stub_auth(json=client_fixtures.project_scoped_token(), - base_url=new_auth_url) - - c = client.Client(user_id='c4da488862bd435c9e6c0275a0d0e49a', - password='password', - project_id='225da22d3ce34b15877ea70b2a575f58', - auth_url=self.TEST_URL) - cache = json.dumps(c.auth_ref) - new_client = client.Client(auth_ref=json.loads(cache), - auth_url=new_auth_url) + 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_client.auth_url, new_auth_url) - self.assertEqual(new_client.username, 'exampleuser') + 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): - self.stub_auth(json=client_fixtures.trust_token()) - - c = client.Client(user_domain_name='exampledomain', - username='exampleuser', - password='password', - auth_url=self.TEST_URL, - trust_id='fe0aef') + 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(c.auth_ref.trust_id, 'fe0aef') - self.assertEqual(c.auth_ref.trustee_user_id, '0ca8f6') - self.assertEqual(c.auth_ref.trustor_user_id, 'bd263c') + 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(c.auth_user_id, '0ca8f6') + self.assertEqual(token.user_id, c.auth_user_id) def test_init_err_no_auth_url(self): - self.assertRaises(exceptions.AuthorizationFailure, - client.Client, - username='exampleuser', - password='password') + # 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) @@ -165,13 +189,16 @@ def _management_url_is_updated(self, fixture, **kwargs): self.stub_auth(response_list=[{'json': fixture}, {'json': second}]) - cl = client.Client(username='exampleuser', - password='password', - auth_url=self.TEST_URL, - **kwargs) + # 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) - cl.authenticate() + 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): @@ -185,12 +212,13 @@ def test_management_url_is_updated_with_domain(self): 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', - tenant_name='exampleproject', + project_name='exampleproject', auth_url=self.TEST_URL, region_name='North') self.assertEqual(cl.service_catalog.url_for(service_type='image'), @@ -198,20 +226,26 @@ def test_client_with_region_name_passes_to_service_catalog(self): cl = client.Client(username='exampleuser', password='password', - tenant_name='exampleproject', + 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): - self.assertRaises(exceptions.AuthorizationFailure, - client.Client, - project_name='exampleproject', - auth_url=self.TEST_URL) + # 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): - opts = {'auth': token_endpoint.Token('a', 'b'), + 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, @@ -220,11 +254,17 @@ def test_client_params(self): 'user_agent': uuid.uuid4().hex, } - sess = session.Session() cl = client.Client(session=sess, **opts) - for k, v in six.iteritems(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 index 752f25ac5..0efc3dfbf 100644 --- a/keystoneclient/tests/unit/v3/test_credentials.py +++ b/keystoneclient/tests/unit/v3/test_credentials.py @@ -16,7 +16,7 @@ from keystoneclient.v3 import credentials -class CredentialTests(utils.TestCase, utils.CrudTests): +class CredentialTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(CredentialTests, self).setUp() self.key = 'credential' @@ -31,24 +31,3 @@ def new_ref(self, **kwargs): kwargs.setdefault('type', uuid.uuid4().hex) kwargs.setdefault('user_id', uuid.uuid4().hex) return kwargs - - @staticmethod - def _ref_data_not_blob(ref): - ret_ref = ref.copy() - ret_ref['data'] = ref['blob'] - del ret_ref['blob'] - return ret_ref - - def test_create_data_not_blob(self): - # Test create operation with previous, deprecated "data" argument, - # which should be translated into "blob" at the API call level - req_ref = self.new_ref() - api_ref = self._ref_data_not_blob(req_ref) - req_ref.pop('id') - self.test_create(api_ref, req_ref) - - def test_update_data_not_blob(self): - # Likewise test update operation with data instead of blob argument - req_ref = self.new_ref() - api_ref = self._ref_data_not_blob(req_ref) - self.test_update(api_ref, req_ref) diff --git a/keystoneclient/tests/unit/v3/test_discover.py b/keystoneclient/tests/unit/v3/test_discover.py index ae88bb457..f54b2f9c4 100644 --- a/keystoneclient/tests/unit/v3/test_discover.py +++ b/keystoneclient/tests/unit/v3/test_discover.py @@ -27,12 +27,12 @@ def setUp(self): "href": "http://127.0.0.1:5000/v3.0/", }, {"rel": "describedby", "type": "text/html", - "href": "http://docs.openstack.org/api/" + "href": "https://docs.openstack.org/api/" "openstack-identity-service/3/" "content/", }, {"rel": "describedby", "type": "application/pdf", - "href": "http://docs.openstack.org/api/" + "href": "https://docs.openstack.org/api/" "openstack-identity-service/3/" "identity-dev-guide-3.pdf", }, ]}, @@ -44,12 +44,12 @@ 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", } ]}], @@ -65,7 +65,9 @@ def test_get_version_local(self): status_code=300, 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) 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 index 9cc23e7eb..72a7b8833 100644 --- a/keystoneclient/tests/unit/v3/test_domains.py +++ b/keystoneclient/tests/unit/v3/test_domains.py @@ -16,7 +16,7 @@ from keystoneclient.v3 import domains -class DomainTests(utils.TestCase, utils.CrudTests): +class DomainTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(DomainTests, self).setUp() self.key = 'domain' @@ -30,6 +30,12 @@ def new_ref(self, **kwargs): 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') diff --git a/keystoneclient/tests/unit/v3/test_ec2.py b/keystoneclient/tests/unit/v3/test_ec2.py index ff463b0a8..66679f605 100644 --- a/keystoneclient/tests/unit/v3/test_ec2.py +++ b/keystoneclient/tests/unit/v3/test_ec2.py @@ -14,7 +14,7 @@ from keystoneclient.v3 import ec2 -class EC2Tests(utils.TestCase): +class EC2Tests(utils.ClientTestCase): def test_create(self): user_id = 'usr' diff --git a/keystoneclient/tests/unit/v3/test_endpoint_filter.py b/keystoneclient/tests/unit/v3/test_endpoint_filter.py index eaca264c7..62e89cb35 100644 --- a/keystoneclient/tests/unit/v3/test_endpoint_filter.py +++ b/keystoneclient/tests/unit/v3/test_endpoint_filter.py @@ -36,8 +36,15 @@ def new_endpoint_ref(self, **kwargs): 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.TestCase, EndpointTestUtils): + +class EndpointFilterTests(utils.ClientTestCase, EndpointTestUtils): """Test project-endpoint associations (a.k.a. EndpointFilter Extension). Endpoint filter provides associations between service endpoints and @@ -147,3 +154,140 @@ def test_list_projects_for_endpoint(self): 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 index ce9efb4c6..69d90ae5a 100644 --- a/keystoneclient/tests/unit/v3/test_endpoint_policy.py +++ b/keystoneclient/tests/unit/v3/test_endpoint_policy.py @@ -18,7 +18,7 @@ from keystoneclient.tests.unit.v3 import utils -class EndpointPolicyTests(utils.TestCase, +class EndpointPolicyTests(utils.ClientTestCase, test_endpoint_filter.EndpointTestUtils): """Test policy-endpoint associations (a.k.a. EndpointPolicy Extension).""" diff --git a/keystoneclient/tests/unit/v3/test_endpoints.py b/keystoneclient/tests/unit/v3/test_endpoints.py index 000718a68..40fc03be8 100644 --- a/keystoneclient/tests/unit/v3/test_endpoints.py +++ b/keystoneclient/tests/unit/v3/test_endpoints.py @@ -17,7 +17,7 @@ from keystoneclient.v3 import endpoints -class EndpointTests(utils.TestCase, utils.CrudTests): +class EndpointTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(EndpointTests, self).setUp() self.key = 'endpoint' @@ -89,3 +89,16 @@ def test_list_invalid_interface(self): expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) 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 index 4cbae8dc5..08391c7c1 100644 --- a/keystoneclient/tests/unit/v3/test_federation.py +++ b/keystoneclient/tests/unit/v3/test_federation.py @@ -11,12 +11,19 @@ # 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 import exceptions -from keystoneclient import fixture 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 @@ -26,7 +33,7 @@ from keystoneclient.v3 import projects -class IdentityProviderTests(utils.TestCase, utils.CrudTests): +class IdentityProviderTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(IdentityProviderTests, self).setUp() self.key = 'identity_provider' @@ -88,7 +95,7 @@ def test_create(self): self.assertEntityRequestBodyIs(req_ref) -class MappingTests(utils.TestCase, utils.CrudTests): +class MappingTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(MappingTests, self).setUp() self.key = 'mapping' @@ -122,7 +129,7 @@ def test_create(self): self.assertEntityRequestBodyIs(manager_ref) -class ProtocolTests(utils.TestCase, utils.CrudTests): +class ProtocolTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(ProtocolTests, self).setUp() self.key = 'protocol' @@ -132,10 +139,7 @@ def setUp(self): self.path_prefix = 'OS-FEDERATION/identity_providers' def _transform_to_response(self, ref): - """Rebuild dictionary so it can be used as a - reference response body. - - """ + """Construct a response body from a dictionary.""" response = copy.deepcopy(ref) del response['identity_provider'] return response @@ -169,7 +173,7 @@ def test_build_url_provide_base_url(self): 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 + """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`` @@ -278,6 +282,16 @@ def _ref_protocols(): 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} @@ -292,7 +306,7 @@ def test_list_params(self): self.assertQueryStringContains(**filter_kwargs) def test_update(self): - """Test updating federation protocol + """Test updating federation protocol. URL to be tested: PATCH /OS-FEDERATION/identity_providers/ $identity_provider/protocols/$protocol @@ -317,14 +331,14 @@ def test_update(self): self.assertEntityRequestBodyIs(request_body) -class EntityManagerTests(utils.TestCase): +class EntityManagerTests(utils.ClientTestCase): def test_create_object_expect_fail(self): self.assertRaises(TypeError, base.EntityManager, self.client) -class FederationProjectTests(utils.TestCase): +class FederationProjectTests(utils.ClientTestCase): def setUp(self): super(FederationProjectTests, self).setUp() @@ -332,7 +346,7 @@ def setUp(self): self.collection_key = 'projects' self.model = projects.Project self.manager = self.client.federation.projects - self.URL = "%s%s" % (self.TEST_URL, '/OS-FEDERATION/projects') + self.URL = "%s%s" % (self.TEST_URL, '/auth/projects') def new_ref(self, **kwargs): kwargs.setdefault('id', uuid.uuid4().hex) @@ -354,7 +368,102 @@ def test_list_accessible_projects(self): self.assertIsInstance(project, self.model) -class FederationDomainTests(utils.TestCase): +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() @@ -363,7 +472,7 @@ def setUp(self): self.model = domains.Domain self.manager = self.client.federation.domains - self.URL = "%s%s" % (self.TEST_URL, '/OS-FEDERATION/domains') + self.URL = "%s%s" % (self.TEST_URL, '/auth/domains') def new_ref(self, **kwargs): kwargs.setdefault('id', uuid.uuid4().hex) @@ -384,7 +493,7 @@ def test_list_accessible_domains(self): self.assertIsInstance(domain, self.model) -class FederatedTokenTests(utils.TestCase): +class FederatedTokenTests(utils.ClientTestCase): def setUp(self): super(FederatedTokenTests, self).setUp() @@ -403,10 +512,10 @@ def test_get_user_domain_name(self): def test_get_user_domain_id(self): """Ensure a federated user's domain ID does not exist.""" - self.assertIsNone(self.federated_token.user_domain_id) + self.assertEqual('Federated', self.federated_token.user_domain_id) -class ServiceProviderTests(utils.TestCase, utils.CrudTests): +class ServiceProviderTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(ServiceProviderTests, self).setUp() self.key = 'service_provider' @@ -473,3 +582,244 @@ def test_create(self): 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/keystoneclient/tests/unit/v3/test_groups.py b/keystoneclient/tests/unit/v3/test_groups.py index 6ed140cef..90fff0e63 100644 --- a/keystoneclient/tests/unit/v3/test_groups.py +++ b/keystoneclient/tests/unit/v3/test_groups.py @@ -18,7 +18,7 @@ from keystoneclient.v3 import groups -class GroupTests(utils.TestCase, utils.CrudTests): +class GroupTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(GroupTests, self).setUp() self.key = 'group' 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 index b52a75981..a15d94b83 100644 --- a/keystoneclient/tests/unit/v3/test_oauth1.py +++ b/keystoneclient/tests/unit/v3/test_oauth1.py @@ -11,38 +11,34 @@ # 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 -import mock -from oslo_utils import timeutils -import six -from six.moves.urllib import parse as urlparse 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: - import oauthlib from oauthlib import oauth1 except ImportError: oauth1 = None -class BaseTest(utils.TestCase): +class ConsumerTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): - super(BaseTest, self).setUp() if oauth1 is None: self.skipTest('oauthlib package not available') - -class ConsumerTests(BaseTest, utils.CrudTests): - def setUp(self): super(ConsumerTests, self).setUp() self.key = 'consumer' self.collection_key = 'consumers' @@ -80,7 +76,7 @@ def test_description_not_included(self): self.assertEqual(consumer_id, consumer.id) -class TokenTests(BaseTest): +class TokenTests(object): def _new_oauth_token(self): key = uuid.uuid4().hex secret = uuid.uuid4().hex @@ -90,7 +86,7 @@ def _new_oauth_token(self): def _new_oauth_token_with_expires_at(self): key, secret, token = self._new_oauth_token() - expires_at = timeutils.strtime() + expires_at = client_utils.strtime() params = {'oauth_token': key, 'oauth_token_secret': secret, 'oauth_expires_at': expires_at} @@ -98,25 +94,18 @@ def _new_oauth_token_with_expires_at(self): return (key, secret, expires_at, token) def _validate_oauth_headers(self, auth_header, oauth_client): - """Assert that the data in the headers matches the data + """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 ')) - auth_header = auth_header[len('OAuth '):] - # NOTE(stevemar): In newer versions of oauthlib there is - # an additional argument for getting oauth parameters. - # Adding a conditional here to revert back to no arguments - # if an earlier version is detected. - if tuple(oauthlib.__version__.split('.')) > ('0', '6', '1'): - header_params = oauth_client.get_oauth_params(None) - else: - header_params = oauth_client.get_oauth_params() - parameters = dict(header_params) + 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'], six.string_types) + self.assertIsInstance(parameters['oauth_nonce'], str) self.assertEqual(oauth_client.client_key, parameters['oauth_consumer_key']) if oauth_client.resource_owner_key: @@ -128,14 +117,14 @@ def _validate_oauth_headers(self, auth_header, oauth_client): if oauth_client.callback_uri: self.assertEqual(oauth_client.callback_uri, parameters['oauth_callback']) - if oauth_client.timestamp: - self.assertEqual(oauth_client.timestamp, - parameters['oauth_timestamp']) return parameters -class RequestTokenTests(TokenTests): +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 @@ -193,8 +182,11 @@ def test_create_request_token(self): oauth_client) -class AccessTokenTests(TokenTests): +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 @@ -229,13 +221,13 @@ def test_create_access_token_expires_at(self): resource_owner_key=request_key, resource_owner_secret=request_secret, signature_method=oauth1.SIGNATURE_HMAC, - verifier=verifier, - timestamp=expires_at) + verifier=verifier) + self._validate_oauth_headers(req_headers['Authorization'], oauth_client) -class AuthenticateWithOAuthTests(TokenTests): +class AuthenticateWithOAuthTests(utils.TestCase, TokenTests): def setUp(self): super(AuthenticateWithOAuthTests, self).setUp() if oauth1 is None: @@ -255,12 +247,13 @@ def test_oauth_authenticate_success(self): "access_token_id": access_key} self.stub_auth(json=oauth_token) - 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() + 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 = { @@ -285,6 +278,53 @@ def test_oauth_authenticate_success(self): 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): diff --git a/keystoneclient/tests/unit/v3/test_policies.py b/keystoneclient/tests/unit/v3/test_policies.py index a7f8d8afd..47be4838d 100644 --- a/keystoneclient/tests/unit/v3/test_policies.py +++ b/keystoneclient/tests/unit/v3/test_policies.py @@ -16,7 +16,7 @@ from keystoneclient.v3 import policies -class PolicyTests(utils.TestCase, utils.CrudTests): +class PolicyTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(PolicyTests, self).setUp() self.key = 'policy' diff --git a/keystoneclient/tests/unit/v3/test_projects.py b/keystoneclient/tests/unit/v3/test_projects.py index 61a5ef17b..99186f17f 100644 --- a/keystoneclient/tests/unit/v3/test_projects.py +++ b/keystoneclient/tests/unit/v3/test_projects.py @@ -10,14 +10,17 @@ # License for the specific language governing permissions and limitations # under the License. +import fixtures import uuid -from keystoneclient import exceptions +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.TestCase, utils.CrudTests): +class ProjectTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(ProjectTests, self).setUp() self.key = 'project' @@ -52,8 +55,7 @@ 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) + 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)) @@ -61,6 +63,18 @@ def test_list_projects_for_domain(self): 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 @@ -124,14 +138,13 @@ def test_create_with_parent_and_parent_id(self): self.assertEntityRequestBodyIs(ref) def _create_projects_hierarchy(self, hierarchy_size=3): - """Creates a project hierarchy with specified size. + """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] @@ -165,7 +178,7 @@ def test_get_with_subtree_as_ids(self): returned = self.manager.get(ref['id'], subtree_as_ids=True) self.assertQueryStringIs('subtree_as_ids') - self.assertDictEqual(ref['subtree'], returned.subtree) + self.assertEqual(ref['subtree'], returned.subtree) def test_get_with_parents_as_ids(self): projects = self._create_projects_hierarchy() @@ -188,7 +201,7 @@ def test_get_with_parents_as_ids(self): returned = self.manager.get(ref['id'], parents_as_ids=True) self.assertQueryStringIs('parents_as_ids') - self.assertDictEqual(ref['parents'], returned.parents) + self.assertEqual(ref['parents'], returned.parents) def test_get_with_parents_as_ids_and_subtree_as_ids(self): ref = self.new_ref() @@ -210,8 +223,8 @@ def test_get_with_parents_as_ids_and_subtree_as_ids(self): parents_as_ids=True, subtree_as_ids=True) self.assertQueryStringIs('subtree_as_ids&parents_as_ids') - self.assertDictEqual(ref['parents'], returned.parents) - self.assertDictEqual(ref['subtree'], returned.subtree) + 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() @@ -285,7 +298,7 @@ def test_get_with_parents_as_list_and_subtree_as_list(self): 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(exceptions.ValidationError, + self.assertRaises(ksc_exceptions.ValidationError, self.manager.get, project=uuid.uuid4().hex, subtree_as_list=True, @@ -293,7 +306,7 @@ def test_get_with_invalid_parameters_combination(self): # parents_as_list and parents_as_ids can not be included at the # same time in the call. - self.assertRaises(exceptions.ValidationError, + self.assertRaises(ksc_exceptions.ValidationError, self.manager.get, project=uuid.uuid4().hex, parents_as_list=True, @@ -309,5 +322,148 @@ def test_update_with_parent_project(self): # NOTE(rodrigods): this is the expected behaviour of the Identity # server, a different implementation might not fail this request. - self.assertRaises(exceptions.Forbidden, self.manager.update, + 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 index 392f79f97..90ddfdc64 100644 --- a/keystoneclient/tests/unit/v3/test_regions.py +++ b/keystoneclient/tests/unit/v3/test_regions.py @@ -18,7 +18,7 @@ from keystoneclient.v3 import regions -class RegionTests(utils.TestCase, utils.CrudTests): +class RegionTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(RegionTests, self).setUp() self.key = 'region' 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 index 79d2585df..39b4b2355 100644 --- a/keystoneclient/tests/unit/v3/test_role_assignments.py +++ b/keystoneclient/tests/unit/v3/test_role_assignments.py @@ -15,7 +15,7 @@ from keystoneclient.v3 import role_assignments -class RoleAssignmentsTests(utils.TestCase, utils.CrudTests): +class RoleAssignmentsTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(RoleAssignmentsTests, self).setUp() @@ -23,6 +23,32 @@ def setUp(self): 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 @@ -65,12 +91,23 @@ def setUp(self): self.TEST_ALL_RESPONSE_LIST = (self.TEST_USER_PROJECT_LIST + self.TEST_GROUP_PROJECT_LIST + - self.TEST_USER_DOMAIN_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', @@ -112,6 +149,22 @@ def test_project_assignments_list(self): 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', @@ -125,6 +178,50 @@ def test_domain_assignments_list(self): 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', @@ -164,6 +261,18 @@ def test_effective_assignments_list(self): 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', diff --git a/keystoneclient/tests/unit/v3/test_roles.py b/keystoneclient/tests/unit/v3/test_roles.py index 79ac07d5b..0e531e736 100644 --- a/keystoneclient/tests/unit/v3/test_roles.py +++ b/keystoneclient/tests/unit/v3/test_roles.py @@ -20,7 +20,7 @@ from testtools import matchers -class RoleTests(utils.TestCase, utils.CrudTests): +class RoleTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(RoleTests, self).setUp() self.key = 'role' @@ -33,6 +33,32 @@ def new_ref(self, **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 @@ -59,6 +85,20 @@ def test_domain_role_grant_inherited(self): 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 @@ -85,6 +125,20 @@ def test_domain_group_role_grant_inherited(self): 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 @@ -113,6 +167,23 @@ def test_domain_role_list_inherited(self): 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 @@ -141,6 +212,23 @@ def test_domain_group_role_list_inherited(self): 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 @@ -169,6 +257,21 @@ def test_domain_role_check_inherited(self): 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 @@ -197,6 +300,21 @@ def test_domain_group_role_check_inherited(self): 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 @@ -235,6 +353,20 @@ def test_domain_role_revoke_inherited(self): 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 @@ -250,6 +382,21 @@ def test_domain_group_role_revoke_inherited(self): 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 @@ -451,3 +598,227 @@ def test_user_group_role_revoke_fails(self): 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 index 054ad56b3..bdd92cebd 100644 --- a/keystoneclient/tests/unit/v3/test_service_catalog.py +++ b/keystoneclient/tests/unit/v3/test_service_catalog.py @@ -10,9 +10,11 @@ # 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 import fixture +from keystoneclient.tests.unit import utils as test_utils from keystoneclient.tests.unit.v3 import client_fixtures from keystoneclient.tests.unit.v3 import utils @@ -21,9 +23,9 @@ class ServiceCatalogTest(utils.TestCase): def setUp(self): super(ServiceCatalogTest, self).setUp() self.AUTH_RESPONSE_BODY = client_fixtures.auth_response_body() - self.RESPONSE = utils.TestResponse({ - "headers": client_fixtures.AUTH_RESPONSE_HEADERS - }) + self.RESPONSE = test_utils.test_response( + headers=client_fixtures.AUTH_RESPONSE_HEADERS + ) self.north_endpoints = {'public': 'http://glance.north.host/glanceapi/public', @@ -66,16 +68,20 @@ def test_service_catalog_endpoints(self): def test_service_catalog_regions(self): self.AUTH_RESPONSE_BODY['token']['region_name'] = "North" - auth_ref = access.AccessInfo.factory(self.RESPONSE, - self.AUTH_RESPONSE_BODY) + # 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" - auth_ref = access.AccessInfo.factory(self.RESPONSE, - self.AUTH_RESPONSE_BODY) + # 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") @@ -149,7 +155,9 @@ def test_servcie_catalog_get_url_region_names(self): def test_service_catalog_param_overrides_body_region(self): self.AUTH_RESPONSE_BODY['token']['region_name'] = "North" - auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + # 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') diff --git a/keystoneclient/tests/unit/v3/test_services.py b/keystoneclient/tests/unit/v3/test_services.py index 40dde475b..9bda5dbb5 100644 --- a/keystoneclient/tests/unit/v3/test_services.py +++ b/keystoneclient/tests/unit/v3/test_services.py @@ -16,7 +16,7 @@ from keystoneclient.v3 import services -class ServiceTests(utils.TestCase, utils.CrudTests): +class ServiceTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(ServiceTests, self).setUp() self.key = 'service' diff --git a/keystoneclient/tests/unit/v3/test_simple_cert.py b/keystoneclient/tests/unit/v3/test_simple_cert.py index 067083a44..a059f08d7 100644 --- a/keystoneclient/tests/unit/v3/test_simple_cert.py +++ b/keystoneclient/tests/unit/v3/test_simple_cert.py @@ -11,13 +11,15 @@ # 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.TestCase, testresources.ResourcedTestCase): +class SimpleCertTests(utils.ClientTestCase, testresources.ResourcedTestCase): resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] @@ -36,5 +38,36 @@ def test_get_certificates(self): 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 index 2c27fd097..1a8fd6831 100644 --- a/keystoneclient/tests/unit/v3/test_tokens.py +++ b/keystoneclient/tests/unit/v3/test_tokens.py @@ -12,15 +12,15 @@ import uuid +from keystoneauth1 import exceptions import testresources from keystoneclient import access -from keystoneclient import exceptions from keystoneclient.tests.unit import client_fixtures from keystoneclient.tests.unit.v3 import utils -class TokenTests(utils.TestCase, testresources.ResourcedTestCase): +class TokenTests(utils.ClientTestCase, testresources.ResourcedTestCase): resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)] @@ -44,6 +44,24 @@ def test_get_revoked(self): 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): @@ -53,6 +71,10 @@ def test_validate_token_with_token_id(self): 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) @@ -77,6 +99,9 @@ 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) @@ -87,6 +112,11 @@ def test_validate_token_catalog(self): 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() @@ -99,12 +129,30 @@ def test_validate_token_nocatalog(self): 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 index fbd8fdead..1c74ac9b9 100644 --- a/keystoneclient/tests/unit/v3/test_trusts.py +++ b/keystoneclient/tests/unit/v3/test_trusts.py @@ -20,7 +20,7 @@ from keystoneclient.v3.contrib import trusts -class TrustTests(utils.TestCase, utils.CrudTests): +class TrustTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(TrustTests, self).setUp() self.key = 'trust' @@ -64,6 +64,22 @@ def test_create_roles(self): 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 diff --git a/keystoneclient/tests/unit/v3/test_users.py b/keystoneclient/tests/unit/v3/test_users.py index 4619b66ab..a7f03f9ce 100644 --- a/keystoneclient/tests/unit/v3/test_users.py +++ b/keystoneclient/tests/unit/v3/test_users.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock import uuid from keystoneclient import exceptions @@ -19,7 +20,7 @@ from keystoneclient.v3 import users -class UserTests(utils.TestCase, utils.CrudTests): +class UserTests(utils.ClientTestCase, utils.CrudTests): def setUp(self): super(UserTests, self).setUp() self.key = 'user' @@ -111,6 +112,7 @@ def test_create_doesnt_log_password(self): 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], @@ -135,6 +137,8 @@ def test_create_with_project(self): 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', @@ -180,6 +184,8 @@ def test_update_doesnt_log_password(self): 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') @@ -203,6 +209,8 @@ def test_update_with_project(self): 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') @@ -230,8 +238,8 @@ def test_update_password(self): new_password = uuid.uuid4().hex self.stub_url('POST', - [self.collection_key, self.TEST_USER, 'password']) - self.client.user_id = self.TEST_USER + [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 = { @@ -240,12 +248,34 @@ def test_update_password(self): } } - self.assertEqual(self.TEST_URL + '/users/test/password', - self.requests_mock.last_request.url) + 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 diff --git a/keystoneclient/tests/unit/v3/utils.py b/keystoneclient/tests/unit/v3/utils.py index 7f2d633d7..cb3839a9c 100644 --- a/keystoneclient/tests/unit/v3/utils.py +++ b/keystoneclient/tests/unit/v3/utils.py @@ -10,20 +10,20 @@ # License for the specific language governing permissions and limitations # under the License. +import requests import uuid -import six -from six.moves.urllib import parse as urlparse +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 -TestResponse = utils.TestResponse - - def parameterize(ref): - """Rewrites attributes to match the kwarg naming convention in client. + """Rewrite attributes to match the kwarg naming convention in client. >>> parameterize({'project_id': 0}) {'project': 0} @@ -48,6 +48,7 @@ class UnauthenticatedTestCase(utils.TestCase): 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": [{ @@ -127,15 +128,8 @@ class TestCase(UnauthenticatedTestCase): "type": "object-store" }] - def setUp(self): - super(TestCase, self).setUp() - 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 stub_auth(self, subject_token=None, **kwargs): + if not subject_token: subject_token = self.TEST_TOKEN @@ -152,6 +146,44 @@ def stub_auth(self, subject_token=None, **kwargs): 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 @@ -194,6 +226,8 @@ 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') @@ -245,6 +279,20 @@ def _get_expected_path(self, expected_path=None): 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()] @@ -259,7 +307,7 @@ def test_list(self, ref_list=None, expected_path=None, qs_args = self.requests_mock.last_request.qs qs_args_expected = expected_query or filter_kwargs - for key, value in six.iteritems(qs_args_expected): + 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 @@ -302,6 +350,8 @@ def test_find(self, ref=None): 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) @@ -330,3 +380,17 @@ def test_delete(self, ref=None): 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 300ca5422..1c31f2bd3 100644 --- a/keystoneclient/utils.py +++ b/keystoneclient/utils.py @@ -10,135 +10,39 @@ # License for the specific language governing permissions and limitations # under the License. -import functools import getpass import hashlib -import inspect -import logging import sys -from oslo_utils import encodeutils -import prettytable -import six +from keystoneauth1 import exceptions as ksa_exceptions +from oslo_utils import timeutils -from keystoneclient import exceptions - - -logger = logging.getLogger(__name__) - - -# Decorator for cli-args -def arg(*args, **kwargs): - def _decorator(func): - # Because of the semantics 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, print_empty=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] - encoded = encodeutils.safe_encode(pt.get_string(sortby=order_by)) - if six.PY3: - encoded = encoded.decode() - print(encoded) - - -def _word_wrap(string, max_length=0): - """wrap long strings to be no longer than 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, print_empty=False) - pt.aligns = ['l', 'l'] - for (prop, value) in six.iteritems(d): - if value is None: - value = '' - value = _word_wrap(value, max_length=wrap) - pt.add_row([prop, value]) - encoded = encodeutils.safe_encode(pt.get_string(sortby='Property')) - if six.PY3: - encoded = encoded.decode() - print(encoded) +from keystoneclient import exceptions as ksc_exceptions def find_resource(manager, name_or_id): """Helper for the _find_* methods.""" - # first try the entity as a string try: return manager.get(name_or_id) - except (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, six.binary_type): + 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) - except exceptions.NoUniqueMatch: + 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 exceptions.CommandError(msg) - - -def unauthenticated(f): - """Adds 'unauthenticated' attribute to decorated function. - - Usage:: - - @unauthenticated - def mymethod(f): - ... - """ - f.unauthenticated = True - return f - - -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. - """ - return getattr(f, 'unauthenticated', False) + raise ksc_exceptions.CommandError(msg) def hash_signed_token(signed_text, mode='md5'): @@ -148,7 +52,7 @@ def hash_signed_token(signed_text, mode='md5'): def prompt_user_password(): - """Prompt user for a password + """Prompt user for a password. Prompt for a password if stdin is a tty. """ @@ -159,15 +63,18 @@ def prompt_user_password(): # Check for Ctl-D try: password = getpass.getpass('Password: ') - except EOFError: + except EOFError: # nosec(cjschaef): return password, which is None if + # password was not found pass return password def prompt_for_password(): - """Prompt user for password if not provided so the password - doesn't show up in the bash history. + """Prompt user for password if not provided. + + 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 @@ -183,156 +90,33 @@ def prompt_for_password(): return -class positional(object): - """A decorator which enforces only some args may be passed positionally. - - This idea and some of the code was taken from the oauth2 client of the - google-api client. - - This decorator makes it easy to support Python 3 style key-word only - parameters. For example, in Python 3 it is possible to write:: - - def fn(pos1, *, kwonly1, kwonly2=None): - ... - - All named parameters after * must be a keyword:: - - fn(10, 'kw1', 'kw2') # Raises exception. - fn(10, kwonly1='kw1', kwonly2='kw2') # Ok. - - To replicate this behaviour with the positional decorator you simply - specify how many arguments may be passed positionally. To replicate the - example above:: - - @positional(1) - def fn(pos1, kwonly1=None, kwonly2=None): - ... - - If no default value is provided to a keyword argument, it becomes a - required keyword argument:: - - @positional(0) - def fn(required_kw): - ... - - This must be called with the keyword parameter:: - - fn() # Raises exception. - fn(10) # Raises exception. - fn(required_kw=10) # Ok. - - When defining instance or class methods always remember that in python the - first positional argument passed is always the instance so you will need to - account for `self` and `cls`:: - - class MyClass(object): - - @positional(2) - def my_method(self, pos1, kwonly1=None): - ... - - @classmethod - @positional(2) - def my_method(cls, pos1, kwonly1=None): - ... - - If you would prefer not to account for `self` and `cls` you can use the - `method` and `classmethod` helpers which do not consider the initial - positional argument. So the following class is exactly the same as the one - above:: - - class MyClass(object): - - @positional.method(1) - def my_method(self, pos1, kwonly1=None): - ... - - @positional.classmethod(1) - def my_method(cls, pos1, kwonly1=None): - ... - - If a value isn't provided to the decorator then it will enforce that - every variable without a default value will be required to be a kwarg:: - - @positional() - def fn(pos1, kwonly1=None): - ... - - fn(10) # Ok. - fn(10, 20) # Raises exception. - fn(10, kwonly1=20) # Ok. - - This behaviour will work with the `positional.method` and - `positional.classmethod` helper functions as well:: - - class MyClass(object): - - @positional.classmethod() - def my_method(cls, pos1, kwonly1=None): - ... - - MyClass.my_method(10) # Ok. - MyClass.my_method(10, 20) # Raises exception. - MyClass.my_method(10, kwonly1=20) # Ok. - - For compatibility reasons you may wish to not always raise an exception so - a WARN mode is available. Rather than raise an exception a warning message - will be logged:: - - @positional(1, enforcement=positional.WARN): - def fn(pos1, kwonly=1): - ... - - Available modes are: - - - positional.EXCEPT - the default, raise an exception. - - positional.WARN - log a warning on mistake. - """ - - EXCEPT = 'except' - WARN = 'warn' - - def __init__(self, max_positional_args=None, enforcement=EXCEPT): - self._max_positional_args = max_positional_args - self._enforcement = enforcement - - @classmethod - def method(cls, max_positional_args=None, enforcement=EXCEPT): - if max_positional_args is not None: - max_positional_args += 1 - - def f(func): - return cls(max_positional_args, enforcement)(func) - return f - - @classmethod - def classmethod(cls, *args, **kwargs): - def f(func): - return classmethod(cls.method(*args, **kwargs)(func)) - return f - - def __call__(self, func): - if self._max_positional_args is None: - spec = inspect.getargspec(func) - self._max_positional_args = len(spec.args) - len(spec.defaults) - - plural = '' if self._max_positional_args == 1 else 's' - - @functools.wraps(func) - def inner(*args, **kwargs): - if len(args) > self._max_positional_args: - message = ('%(name)s takes at most %(max)d positional ' - 'argument%(plural)s (%(given)d given)' % - {'name': func.__name__, - 'max': self._max_positional_args, - 'given': len(args), - 'plural': plural}) - - if self._enforcement == self.EXCEPT: - raise TypeError(message) - elif self._enforcement == self.WARN: - logger.warning(message) - - return func(*args, **kwargs) - - return inner +_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 30407e0c6..23382fea1 100644 --- a/keystoneclient/v2_0/__init__.py +++ b/keystoneclient/v2_0/__init__.py @@ -1,6 +1,6 @@ from keystoneclient.v2_0.client import Client # noqa -__all__ = [ +__all__ = ( 'client', -] +) diff --git a/keystoneclient/v2_0/certificates.py b/keystoneclient/v2_0/certificates.py index b8d2573f0..2c69dfb3d 100644 --- a/keystoneclient/v2_0/certificates.py +++ b/keystoneclient/v2_0/certificates.py @@ -25,7 +25,6 @@ def get_ca_certificate(self): :rtype: str """ - resp, body = self._client.get('/certificates/ca', authenticated=False) return resp.text @@ -36,7 +35,6 @@ def get_signing_certificate(self): :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 bba556e83..904f76935 100644 --- a/keystoneclient/v2_0/client.py +++ b/keystoneclient/v2_0/client.py @@ -14,6 +14,7 @@ # under the License. import logging +import warnings from keystoneclient.auth.identity import v2 as v2_auth from keystoneclient import exceptions @@ -79,6 +80,11 @@ class Client(httpclient.HTTPClient): 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, @@ -87,11 +93,15 @@ class Client(httpclient.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) @@ -102,11 +112,15 @@ class Client(httpclient.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) @@ -118,11 +132,14 @@ class Client(httpclient.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') - >>> admin_client.tenants.list() + >>> 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() """ @@ -130,6 +147,13 @@ class Client(httpclient.HTTPClient): def __init__(self, **kwargs): """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) + super(Client, self).__init__(**kwargs) self.certificates = certificates.CertificatesManager(self._adapter) diff --git a/keystoneclient/v2_0/ec2.py b/keystoneclient/v2_0/ec2.py index 0baf224f9..0abe98b07 100644 --- a/keystoneclient/v2_0/ec2.py +++ b/keystoneclient/v2_0/ec2.py @@ -18,6 +18,7 @@ class EC2(base.Resource): def __repr__(self): + """Return string representation of EC2 resource information.""" return "" % self._info def delete(self): @@ -32,11 +33,10 @@ def create(self, user_id, tenant_id): :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. diff --git a/keystoneclient/v2_0/endpoints.py b/keystoneclient/v2_0/endpoints.py index 35bda258f..13ac399d1 100644 --- a/keystoneclient/v2_0/endpoints.py +++ b/keystoneclient/v2_0/endpoints.py @@ -18,7 +18,9 @@ class Endpoint(base.Resource): """Represents a Keystone endpoint.""" + def __repr__(self): + """Return string representation of endpoint resource information.""" return "" % self._info @@ -39,7 +41,7 @@ def create(self, region, service_id, publicurl, adminurl=None, '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.""" diff --git a/keystoneclient/v2_0/extensions.py b/keystoneclient/v2_0/extensions.py index b805d31de..f1c402700 100644 --- a/keystoneclient/v2_0/extensions.py +++ b/keystoneclient/v2_0/extensions.py @@ -15,7 +15,9 @@ class Extension(base.Resource): """Represents an Identity API extension.""" + def __repr__(self): + """Return string representation of extension resource information.""" return "" % self._info diff --git a/keystoneclient/v2_0/roles.py b/keystoneclient/v2_0/roles.py index 3fc47e96f..b91808446 100644 --- a/keystoneclient/v2_0/roles.py +++ b/keystoneclient/v2_0/roles.py @@ -19,7 +19,9 @@ class Role(base.Resource): """Represents a Keystone role.""" + def __repr__(self): + """Return string representation of role resource information.""" return "" % self._info def delete(self): @@ -28,6 +30,7 @@ def delete(self): class RoleManager(base.ManagerWithFind): """Manager class for manipulating Keystone roles.""" + resource_class = Role def get(self, role): @@ -36,7 +39,7 @@ def get(self, role): def create(self, name): """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.""" @@ -56,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. @@ -72,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 adb740d57..8f20b4daa 100644 --- a/keystoneclient/v2_0/services.py +++ b/keystoneclient/v2_0/services.py @@ -19,12 +19,15 @@ class Service(base.Resource): """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.""" + resource_class = Service def list(self): @@ -35,12 +38,12 @@ def get(self, id): """Retrieve a service by id.""" return self._get("/OS-KSADM/services/%s" % id, "OS-KSADM:service") - def create(self, name, service_type, description): + 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.""" diff --git a/keystoneclient/v2_0/shell.py b/keystoneclient/v2_0/shell.py deleted file mode 100755 index 9d7d7ce6d..000000000 --- a/keystoneclient/v2_0/shell.py +++ /dev/null @@ -1,546 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack Foundation -# 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. -""" -This module is pending deprecation in favor of python-openstackclient. - -Bug fixes are welcome, but new features should be exposed to the CLI by -python-openstackclient after being added to the python-keystoneclient library. - -""" - -import argparse -import getpass -import sys - -from oslo_utils import strutils -import six - -from keystoneclient.i18n import _ -from keystoneclient import utils -from keystoneclient.v2_0 import client - - -CLIENT_CLASS = client.Client -ASK_FOR_PASSWORD = object() - - -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) - - # Change __doc__ attribute back to origin function's __doc__ - wrapped.__doc__ = f.__doc__ - - return wrapped - - -@utils.arg('--tenant', '--tenant-id', metavar='', - help='Tenant; lists all users if not specified.') -@utils.arg('--tenant_id', help=argparse.SUPPRESS) -def do_user_list(kc, args): - """List users.""" - if args.tenant: - tenant_id = utils.find_resource(kc.tenants, args.tenant).id - else: - tenant_id = None - users = kc.users.list(tenant_id=tenant_id) - utils.print_list(users, ['id', 'name', 'enabled', 'email'], - order_by='name') - - -@utils.arg('user', metavar='', help='Name or ID of user to display.') -def do_user_get(kc, args): - """Display user details.""" - user = utils.find_resource(kc.users, args.user) - utils.print_dict(user._info) - - -@utils.arg('--name', metavar='', required=True, - help='New user name (must be unique).') -@utils.arg('--tenant', '--tenant-id', metavar='', - help='New user default tenant.') -@utils.arg('--tenant_id', help=argparse.SUPPRESS) -@utils.arg('--pass', metavar='', dest='passwd', nargs='?', - const=ASK_FOR_PASSWORD, help='New user password; ' - 'required for some auth backends.') -@utils.arg('--email', metavar='', - help='New user email address.') -@utils.arg('--enabled', metavar='', default=True, - help='Initial user enabled status. Default is true.') -def do_user_create(kc, args): - """Create new user.""" - if args.tenant: - tenant_id = utils.find_resource(kc.tenants, args.tenant).id - elif args.tenant_id: - tenant_id = args.tenant_id - else: - tenant_id = None - new_passwd = args.passwd - if args.passwd is ASK_FOR_PASSWORD: - new_passwd = utils.prompt_for_password() - user = kc.users.create(args.name, new_passwd, args.email, - tenant_id=tenant_id, - enabled=strutils.bool_from_string(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('user', metavar='', help='Name or ID of user 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 is not None: - kwargs['email'] = args.email - if args.enabled: - kwargs['enabled'] = strutils.bool_from_string(args.enabled) - - if not len(kwargs): - print(_("User not updated, no arguments present.")) - return - - user = utils.find_resource(kc.users, args.user) - try: - kc.users.update(user, **kwargs) - print(_('User has been updated.')) - except Exception as e: - print(_('Unable to update user: %s') % e) - - -@utils.arg('--pass', metavar='', dest='passwd', required=False, - help='Desired new password.') -@utils.arg('user', metavar='', - help='Name or ID of user to update password.') -def do_user_password_update(kc, args): - """Update user password.""" - user = utils.find_resource(kc.users, args.user) - new_passwd = args.passwd or utils.prompt_for_password() - if new_passwd is None: - msg = (_("\nPlease specify password using the --pass option " - "or using the prompt")) - sys.exit(msg) - kc.users.update_password(user, new_passwd) - - -@utils.arg('--current-password', metavar='', - dest='currentpasswd', required=False, help='Current password, ' - 'Defaults to the password as set by --os-password or ' - 'env[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('user', metavar='', help='Name or ID of user to delete.') -def do_user_delete(kc, args): - """Delete user.""" - user = utils.find_resource(kc.users, args.user) - kc.users.delete(user) - - -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('tenant', metavar='', - help='Name or ID of tenant to display.') -def do_tenant_get(kc, args): - """Display tenant details.""" - tenant = utils.find_resource(kc.tenants, args.tenant) - 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 is true.') -def do_tenant_create(kc, args): - """Create new tenant.""" - tenant = kc.tenants.create(args.name, - description=args.description, - enabled=strutils.bool_from_string(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('tenant', metavar='', - help='Name or ID of tenant to update.') -def do_tenant_update(kc, args): - """Update tenant name, description, enabled status.""" - tenant = utils.find_resource(kc.tenants, args.tenant) - 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': strutils.bool_from_string(args.enabled)}) - - if kwargs == {}: - print(_("Tenant not updated, no arguments present.")) - return - tenant.update(**kwargs) - - -@utils.arg('tenant', metavar='', - help='Name or ID of tenant to delete.') -def do_tenant_delete(kc, args): - """Delete tenant.""" - tenant = utils.find_resource(kc.tenants, args.tenant) - kc.tenants.delete(tenant) - - -@utils.arg('--type', metavar='', required=True, - help='Service type (one of: identity, compute, network, ' - 'image, object-store, or other service identifier string).') -@utils.arg('--name', metavar='', - help='Name of new service (must be unique).') -@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('service', metavar='', - help='Name or ID of service to display.') -def do_service_get(kc, args): - """Display service from Service Catalog.""" - service = utils.find_resource(kc.services, args.service) - utils.print_dict(service._info) - - -@utils.arg('service', metavar='', - help='Name or ID of service to delete.') -def do_service_delete(kc, args): - """Delete service from Service Catalog.""" - service = utils.find_resource(kc.services, args.service) - kc.services.delete(service.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('role', metavar='', help='Name or ID of role to display.') -def do_role_get(kc, args): - """Display role details.""" - role = utils.find_resource(kc.roles, args.role) - 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('role', metavar='', help='Name or ID of role to delete.') -def do_role_delete(kc, args): - """Delete role.""" - role = utils.find_resource(kc.roles, args.role) - kc.roles.delete(role) - - -@utils.arg('--user', '--user-id', '--user_id', metavar='', - required=True, help='Name or ID of user.') -@utils.arg('--role', '--role-id', '--role_id', metavar='', - required=True, help='Name or ID of role.') -@utils.arg('--tenant', '--tenant-id', metavar='', - help='Name or ID of tenant.') -@utils.arg('--tenant_id', help=argparse.SUPPRESS) -def do_user_role_add(kc, args): - """Add role to user.""" - user = utils.find_resource(kc.users, args.user) - role = utils.find_resource(kc.roles, args.role) - if args.tenant: - tenant = utils.find_resource(kc.tenants, args.tenant) - elif args.tenant_id: - tenant = args.tenant_id - else: - tenant = None - kc.roles.add_user_role(user, role, tenant) - - -@utils.arg('--user', '--user-id', '--user_id', metavar='', - required=True, help='Name or ID of user.') -@utils.arg('--role', '--role-id', '--role_id', metavar='', - required=True, help='Name or ID of role.') -@utils.arg('--tenant', '--tenant-id', metavar='', - help='Name or ID of tenant.') -@utils.arg('--tenant_id', help=argparse.SUPPRESS) -def do_user_role_remove(kc, args): - """Remove role from user.""" - user = utils.find_resource(kc.users, args.user) - role = utils.find_resource(kc.roles, args.role) - if args.tenant: - tenant = utils.find_resource(kc.tenants, args.tenant) - elif args.tenant_id: - tenant = args.tenant_id - else: - tenant = None - kc.roles.remove_user_role(user, role, tenant) - - -@utils.arg('--user', '--user-id', metavar='', - help='List roles granted to specified user.') -@utils.arg('--user_id', help=argparse.SUPPRESS) -@utils.arg('--tenant', '--tenant-id', metavar='', - help='List only roles granted on specified tenant.') -@utils.arg('--tenant_id', help=argparse.SUPPRESS) -def do_user_role_list(kc, args): - """List roles granted to a user.""" - if args.tenant: - tenant_id = utils.find_resource(kc.tenants, args.tenant).id - elif args.tenant_id: - tenant_id = args.tenant_id - else: - # use the authenticated tenant id as a default - tenant_id = kc.auth_tenant_id - - if args.user: - user_id = utils.find_resource(kc.users, args.user).id - elif args.user_id: - user_id = args.user_id - else: - # use the authenticated user id as a default - user_id = kc.auth_user_id - roles = kc.roles.roles_for_user(user=user_id, tenant=tenant_id) - - # this makes the command output a bit more intuitive - for role in roles: - role.user_id = user_id - role.tenant_id = tenant_id - - utils.print_list(roles, ['id', 'name', 'user_id', 'tenant_id'], - order_by='name') - - -@utils.arg('--user-id', metavar='', - help='User ID for which to create credentials. If not specified, ' - 'the authenticated user will be used.') -@utils.arg('--user_id', help=argparse.SUPPRESS) -@utils.arg('--tenant-id', metavar='', - help='Tenant ID for which to create credentials. If not ' - 'specified, the authenticated tenant ID will be used.') -@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 six.iteritems(endpoints): - 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', '--service-id', '--service_id', - metavar='', required=True, - help='Name or ID of service associated with endpoint.') -@utils.arg('--publicurl', metavar='', required=True, - 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.""" - service_id = utils.find_resource(kc.services, args.service).id - endpoint = kc.endpoints.create(args.region, - 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 79d98d559..91731c45e 100644 --- a/keystoneclient/v2_0/tenants.py +++ b/keystoneclient/v2_0/tenants.py @@ -14,16 +14,15 @@ # License for the specific language governing permissions and limitations # under the License. -import six -from six.moves import urllib +from keystoneauth1 import plugin +import urllib.parse -from keystoneclient import auth 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 @@ -32,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): @@ -72,6 +73,7 @@ def list_users(self): class TenantManager(base.ManagerWithFind): """Manager class for manipulating Keystone tenants.""" + resource_class = Tenant def __init__(self, client, role_manager, user_manager): @@ -89,11 +91,11 @@ def create(self, tenant_name, description=None, enabled=True, **kwargs): "enabled": enabled}} # Allow Extras Passthru and ensure we don't clobber primary arguments. - for k, v in six.iteritems(kwargs): + for k, v in kwargs.items(): if k not in params['tenant']: params['tenant'][k] = v - return self._create('/tenants', params, "tenant") + return self._post('/tenants', params, "tenant") def list(self, limit=None, marker=None): """Get a list of tenants. @@ -105,7 +107,6 @@ def list(self, limit=None, marker=None): :rtype: list of :class:`Tenant` """ - params = {} if limit: params['limit'] = limit @@ -122,7 +123,7 @@ def list(self, limit=None, marker=None): try: tenant_list = self._list('/tenants%s' % query, 'tenants') except exceptions.EndpointNotFound: - endpoint_filter = {'interface': auth.AUTH_INTERFACE} + endpoint_filter = {'interface': plugin.AUTH_INTERFACE} tenant_list = self._list('/tenants%s' % query, 'tenants', endpoint_filter=endpoint_filter) @@ -140,12 +141,12 @@ def update(self, tenant_id, tenant_name=None, description=None, body['tenant']['description'] = description # Allow Extras Passthru and ensure we don't clobber primary arguments. - for k, v in six.iteritems(kwargs): + 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.""" diff --git a/keystoneclient/v2_0/tokens.py b/keystoneclient/v2_0/tokens.py index 670d65b1c..14c054eeb 100644 --- a/keystoneclient/v2_0/tokens.py +++ b/keystoneclient/v2_0/tokens.py @@ -10,16 +10,17 @@ # 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 auth from keystoneclient import base -from keystoneclient import exceptions from keystoneclient.i18n import _ -from keystoneclient import utils class Token(base.Resource): def __repr__(self): + """Return string representation of resource information.""" return "" % self._info @property @@ -38,7 +39,6 @@ def tenant(self): class TokenManager(base.Manager): resource_class = Token - @utils.positional(enforcement=utils.positional.WARN) def authenticate(self, username=None, tenant_id=None, tenant_name=None, password=None, token=None, return_raw=False): if token: @@ -61,10 +61,10 @@ def authenticate(self, username=None, tenant_id=None, tenant_name=None, # no endpoint that can satisfy the request (eg an unscoped token) then # issue it against the auth_url. try: - token_ref = self._create(*args, **kwargs) + token_ref = self._post(*args, **kwargs) except exceptions.EndpointNotFound: - kwargs['endpoint_filter'] = {'interface': auth.AUTH_INTERFACE} - token_ref = self._create(*args, **kwargs) + kwargs['endpoint_filter'] = {'interface': plugin.AUTH_INTERFACE} + token_ref = self._post(*args, **kwargs) return token_ref @@ -84,6 +84,17 @@ def validate(self, 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. @@ -94,19 +105,17 @@ def validate_access_info(self, token): :rtype: :py:class:`keystoneclient.access.AccessInfoV2` """ - def calc_id(token): if isinstance(token, access.AccessInfo): return token.auth_token return base.getid(token) - url = '/tokens/%s' % calc_id(token) - resp, body = self.client.get(url) - access_info = access.AccessInfo.factory(resp=resp, body=body) - return access_info + 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): - """Returns the revoked tokens response. + """Return the revoked tokens response. The response will be a dict containing 'signed' which is a CMS-encoded document. diff --git a/keystoneclient/v2_0/users.py b/keystoneclient/v2_0/users.py index 11e06f3ef..706dafcdc 100644 --- a/keystoneclient/v2_0/users.py +++ b/keystoneclient/v2_0/users.py @@ -14,14 +14,15 @@ # License for the specific language governing permissions and limitations # under the License. -from six.moves import urllib - from keystoneclient import base +import urllib.parse class User(base.Resource): """Represents a Keystone user.""" + def __repr__(self): + """Return string representation of user resource information.""" return "" % self._info def delete(self): @@ -33,6 +34,7 @@ def list_roles(self, tenant=None): class UserManager(base.ManagerWithFind): """Manager class for manipulating Keystone users.""" + resource_class = User def __init__(self, client, role_manager): @@ -50,22 +52,19 @@ def update(self, user, **kwargs): # 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}} + 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}} + params = {"user": {"password": password}} return self._update("/users/%s/OS-KSADM/password" % base.getid(user), params, "user", log=False) @@ -75,7 +74,8 @@ def update_own_password(self, origpasswd, passwd): 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", endpoint_filter={'interface': 'public'}, @@ -83,8 +83,7 @@ def update_own_password(self, origpasswd, passwd): def update_tenant(self, user, tenant): """Update default tenant.""" - params = {"user": {"id": base.getid(user), - "tenantId": base.getid(tenant)}} + params = {"user": {"tenantId": base.getid(tenant)}} # FIXME(ja): seems like a bad url - default tenant is an attribute # not a subresource!??? @@ -99,7 +98,7 @@ def create(self, name, password=None, email=None, "tenantId": tenant_id, "email": email, "enabled": enabled}} - return self._create('/users', params, "user", log=not bool(password)) + return self._post('/users', params, "user", log=not bool(password)) def delete(self, user): """Delete a user.""" @@ -110,7 +109,6 @@ def list(self, tenant_id=None, limit=None, marker=None): :rtype: list of :class:`User` """ - params = {} if limit: params['limit'] = int(limit) diff --git a/keystoneclient/v3/__init__.py b/keystoneclient/v3/__init__.py index 56b26d3b4..fcb00e066 100644 --- a/keystoneclient/v3/__init__.py +++ b/keystoneclient/v3/__init__.py @@ -2,6 +2,6 @@ from keystoneclient.v3.client import Client # noqa -__all__ = [ +__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 34bdfadc6..e99b06549 100644 --- a/keystoneclient/v3/client.py +++ b/keystoneclient/v3/client.py @@ -14,6 +14,7 @@ # under the License. import logging +import warnings from oslo_serialization import jsonutils @@ -21,6 +22,9 @@ 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 @@ -28,13 +32,17 @@ from keystoneclient.v3.contrib import simple_cert from keystoneclient.v3.contrib import trusts from keystoneclient.v3 import credentials +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 @@ -46,8 +54,10 @@ class Client(httpclient.HTTPClient): - """Client for the OpenStack Identity API v3. + 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. @@ -65,11 +75,13 @@ class Client(httpclient.HTTPClient): :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, - use project_name instead. + 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, - use project_id instead. + 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. @@ -80,16 +92,25 @@ class Client(httpclient.HTTPClient): :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(user_domain_name=DOMAIN_NAME, - ... username=USER, - ... password=PASS, - ... project_domain_name=PROJECT_DOMAIN_NAME, - ... project_name=PROJECT_NAME, - ... auth_url=KEYSTONE_URL) - ... + >>> 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) @@ -101,6 +122,10 @@ class Client(httpclient.HTTPClient): :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` @@ -108,12 +133,17 @@ class Client(httpclient.HTTPClient): .. py:attribute:: endpoint_filter :py:class:`keystoneclient.v3.contrib.endpoint_filter.\ -EndpointFilterManager` + EndpointFilterManager` + + .. py:attribute:: endpoint_groups + + :py:class:`keystoneclient.v3.endpoint_groups.\ + EndpointGroupManager` .. py:attribute:: endpoint_policy :py:class:`keystoneclient.v3.contrib.endpoint_policy.\ -EndpointPolicyManager` + EndpointPolicyManager` .. py:attribute:: endpoints @@ -131,6 +161,10 @@ class Client(httpclient.HTTPClient): :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` @@ -143,6 +177,10 @@ class Client(httpclient.HTTPClient): :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` @@ -179,23 +217,44 @@ 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) @@ -244,6 +303,7 @@ def get_raw_token_from_identity_service(self, auth_url, user_id=None, 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 diff --git a/keystoneclient/v3/contrib/__init__.py b/keystoneclient/v3/contrib/__init__.py index 79b89d1a0..576bd6218 100644 --- a/keystoneclient/v3/contrib/__init__.py +++ b/keystoneclient/v3/contrib/__init__.py @@ -1,3 +1,2 @@ -__all__ = [ -] +__all__ = tuple() diff --git a/keystoneclient/v3/contrib/endpoint_filter.py b/keystoneclient/v3/contrib/endpoint_filter.py index 0da79b81b..26d5a8745 100644 --- a/keystoneclient/v3/contrib/endpoint_filter.py +++ b/keystoneclient/v3/contrib/endpoint_filter.py @@ -15,12 +15,19 @@ 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.""" + """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): @@ -39,6 +46,23 @@ def _build_base_url(self, project=None, endpoint=None): 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): @@ -58,7 +82,7 @@ def delete_endpoint_from_project(self, project, endpoint): return super(EndpointFilterManager, self)._delete(url=base_url) def check_endpoint_in_project(self, project, endpoint): - """Checks if project-endpoint association exist.""" + """Check if project-endpoint association exists.""" if not (project and endpoint): raise ValueError(_('project and endpoint are required')) @@ -87,3 +111,53 @@ def list_projects_for_endpoint(self, endpoint): 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 index 24148c1ac..c65b1fbc7 100644 --- a/keystoneclient/v3/contrib/endpoint_policy.py +++ b/keystoneclient/v3/contrib/endpoint_policy.py @@ -39,17 +39,17 @@ def _act_on_policy_association_for_endpoint( def create_policy_association_for_endpoint(self, policy, endpoint): """Create an association between a policy and an endpoint.""" - self._act_on_policy_association_for_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.""" - self._act_on_policy_association_for_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.""" - self._act_on_policy_association_for_endpoint( + return self._act_on_policy_association_for_endpoint( policy, endpoint, self._delete) def _act_on_policy_association_for_service(self, policy, service, action): @@ -67,17 +67,17 @@ def _act_on_policy_association_for_service(self, policy, service, action): def create_policy_association_for_service(self, policy, service): """Create an association between a policy and a service.""" - self._act_on_policy_association_for_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.""" - self._act_on_policy_association_for_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.""" - self._act_on_policy_association_for_service( + return self._act_on_policy_association_for_service( policy, service, self._delete) def _act_on_policy_association_for_region_and_service( @@ -99,19 +99,19 @@ def _act_on_policy_association_for_region_and_service( def create_policy_association_for_region_and_service( self, policy, region, service): """Create an association between a policy and a service in a region.""" - self._act_on_policy_association_for_region_and_service( + 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.""" - self._act_on_policy_association_for_region_and_service( + 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.""" - self._act_on_policy_association_for_region_and_service( + return self._act_on_policy_association_for_region_and_service( policy, region, service, self._delete) def get_policy_for_endpoint(self, endpoint): @@ -130,9 +130,10 @@ def get_policy_for_endpoint(self, endpoint): 'endpoint_id': endpoint_id, 'ext_name': self.OS_EP_POLICY_EXT} - _resp, body = self.client.get(url) - return policies.Policy( - self, body[policies.PolicyManager.key], loaded=True) + 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. diff --git a/keystoneclient/v3/contrib/federation/base.py b/keystoneclient/v3/contrib/federation/base.py index 0160170ea..c09bc2267 100644 --- a/keystoneclient/v3/contrib/federation/base.py +++ b/keystoneclient/v3/contrib/federation/base.py @@ -12,28 +12,28 @@ import abc -import six +from keystoneauth1 import exceptions +from keystoneauth1 import plugin -from keystoneclient.auth import base as base_auth from keystoneclient import base -from keystoneclient import exceptions -@six.add_metaclass(abc.ABCMeta) -class EntityManager(base.Manager): +class EntityManager(base.Manager, metaclass=abc.ABCMeta): """Manager class for listing federated accessible objects.""" + resource_class = None - @abc.abstractproperty + @property + @abc.abstractmethod def object_type(self): raise exceptions.MethodNotImplemented def list(self): - url = '/OS-FEDERATION/%s' % self.object_type + url = '/auth/%s' % self.object_type try: tenant_list = self._list(url, self.object_type) - except exceptions.EndpointNotFound: - endpoint_filter = {'interface': base_auth.AUTH_INTERFACE} + 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/identity_providers.py b/keystoneclient/v3/contrib/federation/identity_providers.py index 242d5d962..221ec11f4 100644 --- a/keystoneclient/v3/contrib/federation/identity_providers.py +++ b/keystoneclient/v3/contrib/federation/identity_providers.py @@ -11,16 +11,16 @@ # under the License. from keystoneclient import base -from keystoneclient import utils class IdentityProvider(base.Resource): - """Object representing Identity Provider container + """Object representing Identity Provider container. Attributes: * id: user-defined unique string identifying Identity Provider. """ + pass @@ -38,7 +38,6 @@ def _build_url_and_put(self, **kwargs): return self._update(url, body=body, response_key=self.key, method='PUT') - @utils.positional.method(0) def create(self, id, **kwargs): """Create Identity Provider object. @@ -46,19 +45,25 @@ def create(self, id, **kwargs): 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 + """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( @@ -70,6 +75,9 @@ def list(self, **kwargs): Utilize Keystone URI: GET /OS-FEDERATION/identity_providers + :returns: a list of IdentityProvider resource objects. + :rtype: List + """ return super(IdentityProviderManager, self).list(**kwargs) @@ -81,6 +89,8 @@ def update(self, identity_provider, **kwargs): :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( @@ -92,8 +102,8 @@ def delete(self, identity_provider): Utilize Keystone URI: DELETE /OS-FEDERATION/identity_providers/$identity_provider - :param identity_provider: an object with identity_provider_id - stored inside. + :param identity_provider: the Identity Provider ID itself or an object + with it stored inside. """ return super(IdentityProviderManager, self).delete( diff --git a/keystoneclient/v3/contrib/federation/mappings.py b/keystoneclient/v3/contrib/federation/mappings.py index 1cdb87934..a0e54ae6f 100644 --- a/keystoneclient/v3/contrib/federation/mappings.py +++ b/keystoneclient/v3/contrib/federation/mappings.py @@ -11,16 +11,16 @@ # under the License. from keystoneclient import base -from keystoneclient import utils class Mapping(base.Resource): - """An object representing mapping container + """An object representing mapping container. Attributes: * id: user defined unique string identifying mapping. """ + pass @@ -39,7 +39,6 @@ def _build_url_and_put(self, **kwargs): response_key=self.key, method='PUT') - @utils.positional.method(0) def create(self, mapping_id, **kwargs): """Create federation mapping. @@ -94,7 +93,7 @@ def list(self, **kwargs): """List all federation mappings. Utilize Identity API operation: - GET /OS-FEDERATION/mappings/$mapping_id + GET /OS-FEDERATION/mappings """ return super(MappingManager, self).list(**kwargs) diff --git a/keystoneclient/v3/contrib/federation/protocols.py b/keystoneclient/v3/contrib/federation/protocols.py index 97d88297b..4fad689fe 100644 --- a/keystoneclient/v3/contrib/federation/protocols.py +++ b/keystoneclient/v3/contrib/federation/protocols.py @@ -11,7 +11,6 @@ # under the License. from keystoneclient import base -from keystoneclient import utils class Protocol(base.Resource): @@ -22,6 +21,7 @@ class Protocol(base.Resource): federation protocol. """ + pass @@ -35,7 +35,6 @@ class ProtocolManager(base.CrudManager): 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 = {} @@ -56,7 +55,6 @@ def _build_url_and_put(self, request_body=None, **kwargs): response_key=self.key, method='PUT') - @utils.positional.method(3) def create(self, protocol_id, identity_provider, mapping, **kwargs): """Create federation protocol object and tie to the Identity Provider. diff --git a/keystoneclient/v3/contrib/federation/saml.py b/keystoneclient/v3/contrib/federation/saml.py index b7583546a..435e45dc6 100644 --- a/keystoneclient/v3/contrib/federation/saml.py +++ b/keystoneclient/v3/contrib/federation/saml.py @@ -34,11 +34,10 @@ def create_saml_assertion(self, service_provider, token_id): :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 resp.text + 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. @@ -54,11 +53,10 @@ def create_ecp_assertion(self, service_provider, token_id): :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 resp.text + return self._prepare_return_value(resp, resp.text) def _create_common_request(self, service_provider, token_id): headers = {'Content-Type': 'application/json'} diff --git a/keystoneclient/v3/contrib/federation/service_providers.py b/keystoneclient/v3/contrib/federation/service_providers.py index a4192956b..fed1257d1 100644 --- a/keystoneclient/v3/contrib/federation/service_providers.py +++ b/keystoneclient/v3/contrib/federation/service_providers.py @@ -11,11 +11,10 @@ # under the License. from keystoneclient import base -from keystoneclient import utils class ServiceProvider(base.Resource): - """Object representing Service Provider container + """Object representing Service Provider container. Attributes: * id: user-defined unique string identifying Service Provider. @@ -23,6 +22,7 @@ class ServiceProvider(base.Resource): * auth_url: the authentication url of Service Provider. """ + pass @@ -40,7 +40,6 @@ def _build_url_and_put(self, **kwargs): return self._update(url, body=body, response_key=self.key, method='PUT') - @utils.positional.method(0) def create(self, id, **kwargs): """Create Service Provider object. @@ -54,7 +53,7 @@ def create(self, id, **kwargs): **kwargs) def get(self, service_provider): - """Fetch Service Provider object + """Fetch Service Provider object. Utilize Keystone URI: ``GET /OS-FEDERATION/service_providers/{id}`` diff --git a/keystoneclient/v3/contrib/oauth1/access_tokens.py b/keystoneclient/v3/contrib/oauth1/access_tokens.py index 12b0c6bc6..a64c5dddc 100644 --- a/keystoneclient/v3/contrib/oauth1/access_tokens.py +++ b/keystoneclient/v3/contrib/oauth1/access_tokens.py @@ -11,9 +11,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import unicode_literals +from keystoneauth1 import plugin -from keystoneclient import auth from keystoneclient import base from keystoneclient.v3.contrib.oauth1 import utils @@ -29,6 +28,7 @@ class AccessToken(base.Resource): class AccessTokenManager(base.CrudManager): """Manager class for manipulating identity OAuth access tokens.""" + resource_class = AccessToken def create(self, consumer_key, consumer_secret, request_key, @@ -40,9 +40,11 @@ def create(self, consumer_key, consumer_secret, request_key, resource_owner_secret=request_secret, signature_method=oauth1.SIGNATURE_HMAC, verifier=verifier) - url = self.api.get_endpoint(interface=auth.AUTH_INTERFACE).rstrip('/') + 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.resource_class(self, token) + return self._prepare_return_value(resp, + self.resource_class(self, token)) diff --git a/keystoneclient/v3/contrib/oauth1/consumers.py b/keystoneclient/v3/contrib/oauth1/consumers.py index 25e8ca191..5c81ff47f 100644 --- a/keystoneclient/v3/contrib/oauth1/consumers.py +++ b/keystoneclient/v3/contrib/oauth1/consumers.py @@ -22,11 +22,13 @@ class Consumer(base.Resource): * 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' diff --git a/keystoneclient/v3/contrib/oauth1/core.py b/keystoneclient/v3/contrib/oauth1/core.py index 8d1032577..4b0278e13 100644 --- a/keystoneclient/v3/contrib/oauth1/core.py +++ b/keystoneclient/v3/contrib/oauth1/core.py @@ -58,6 +58,7 @@ class OAuthManagerOptionalImportProxy(object): """ 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) diff --git a/keystoneclient/v3/contrib/oauth1/request_tokens.py b/keystoneclient/v3/contrib/oauth1/request_tokens.py index 33ecc3ad7..7494e8349 100644 --- a/keystoneclient/v3/contrib/oauth1/request_tokens.py +++ b/keystoneclient/v3/contrib/oauth1/request_tokens.py @@ -11,11 +11,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import unicode_literals +import urllib.parse as urlparse -from six.moves.urllib import parse as urlparse +from keystoneauth1 import plugin -from keystoneclient import auth from keystoneclient import base from keystoneclient.v3.contrib.oauth1 import utils @@ -38,6 +37,7 @@ def authorize(self, roles): class RequestTokenManager(base.CrudManager): """Manager class for manipulating identity OAuth request tokens.""" + resource_class = RequestToken def authorize(self, request_token, roles): @@ -50,7 +50,6 @@ def authorize(self, request_token, roles): 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]} @@ -63,10 +62,12 @@ def create(self, consumer_key, consumer_secret, project): client_secret=consumer_secret, signature_method=oauth1.SIGNATURE_HMAC, callback_uri="oob") - url = self.api.get_endpoint(interface=auth.AUTH_INTERFACE).rstrip("/") + 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.resource_class(self, token) + 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 index bf741ab4e..40a0632af 100644 --- a/keystoneclient/v3/contrib/oauth1/utils.py +++ b/keystoneclient/v3/contrib/oauth1/utils.py @@ -11,23 +11,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -import six -from six.moves.urllib import parse as urlparse +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 + """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. """ - - if six.PY3: - body = body.decode('utf-8') + body = body.decode('utf-8') credentials = urlparse.parse_qs(body) key = credentials['oauth_token'][0] diff --git a/keystoneclient/v3/contrib/simple_cert.py b/keystoneclient/v3/contrib/simple_cert.py index c76234a3d..6b58b27b3 100644 --- a/keystoneclient/v3/contrib/simple_cert.py +++ b/keystoneclient/v3/contrib/simple_cert.py @@ -11,12 +11,15 @@ # 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. @@ -25,10 +28,9 @@ def get_ca_certificates(self): :rtype: str """ - resp, body = self._client.get('/OS-SIMPLE-CERT/ca', authenticated=False) - return resp.text + return self.mgr._prepare_return_value(resp, resp.text) def get_certificates(self): """Get signing certificates. @@ -37,7 +39,6 @@ def get_certificates(self): :rtype: str """ - resp, body = self._client.get('/OS-SIMPLE-CERT/certificates', authenticated=False) - return resp.text + return self.mgr._prepare_return_value(resp, resp.text) diff --git a/keystoneclient/v3/contrib/trusts.py b/keystoneclient/v3/contrib/trusts.py index 5fe88f8c9..a8ef57909 100644 --- a/keystoneclient/v3/contrib/trusts.py +++ b/keystoneclient/v3/contrib/trusts.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_utils import timeutils - from keystoneclient import base from keystoneclient import exceptions from keystoneclient.i18n import _ +from keystoneclient import utils class Trust(base.Resource): @@ -27,24 +26,27 @@ class Trust(base.Resource): * 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, - project=None, impersonation=False, expires_at=None, - remaining_uses=None, **kwargs): + 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 @@ -54,14 +56,18 @@ def create(self, trustee_user, trustor_user, role_names=None, """ # Convert role_names list into list-of-dict API format + roles = [] if role_names: - roles = [{'name': n} for n in role_names] - else: + 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 = timeutils.isotime(at=expires_at, subsecond=True) + expires_str = utils.isotime(at=expires_at, subsecond=True) else: expires_str = None diff --git a/keystoneclient/v3/credentials.py b/keystoneclient/v3/credentials.py index 8ef7ce428..70e067001 100644 --- a/keystoneclient/v3/credentials.py +++ b/keystoneclient/v3/credentials.py @@ -15,8 +15,6 @@ # under the License. from keystoneclient import base -from keystoneclient.i18n import _ -from keystoneclient import utils class Credential(base.Resource): @@ -24,63 +22,59 @@ class Credential(base.Resource): Attributes: * id: a uuid that identifies the credential - * user_id: user ID - * type: credential type - * blob: credential data - * project_id: project ID (optional) + * 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 _get_data_blob(self, blob, data): - # Ref bug #1259461, the <= 0.4.1 keystoneclient calling convention was - # to pass "data", but the underlying API expects "blob", so - # support both in the python API for backwards compatibility - if blob is not None: - return blob - elif data is not None: - # FIXME(shardy): Passing data is deprecated. Provide an - # appropriate warning. - return data - else: - raise ValueError( - _("Credential requires blob to be specified")) - - @utils.positional(1, enforcement=utils.positional.WARN) - def create(self, user, type, blob=None, data=None, project=None, **kwargs): - """Create a credential - - :param user: User - :type user: :class:`keystoneclient.v3.users.User` or str - :param str type: credential type, should be either ``ec2`` or ``cert`` - :param JSON blob: Credential data - :param JSON data: Deprecated, use blob instead. - :param project: Project, optional - :type project: :class:`keystoneclient.v3.projects.Project` or str - :param kwargs: Extra attributes passed to create. - - :raises ValueError: if one of ``blob`` or ``data`` is not specified. + 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, - blob=self._get_data_blob(blob, data), + blob=blob, project_id=base.getid(project), **kwargs) def get(self, credential): - """Get a credential + """Retrieve a credential. + + :param credential: the credential to be retrieved from the server + :type credential: str or + :class:`keystoneclient.v3.credentials.Credential` - :param credential: Credential - :type credential: :class:`Credential` or str + :returns: the specified credential + :rtype: :class:`keystoneclient.v3.credentials.Credential` """ return super(CredentialManager, self).get( @@ -89,45 +83,57 @@ def get(self, credential): def list(self, **kwargs): """List credentials. - If ``**kwargs`` are provided, then filter credentials with - attributes matching ``**kwargs``. - """ - return super(CredentialManager, self).list(**kwargs) + :param kwargs: If user_id or type is specified then credentials + will be filtered accordingly. - @utils.positional(2, enforcement=utils.positional.WARN) - def update(self, credential, user, type=None, blob=None, data=None, - project=None, **kwargs): - """Update a credential + :returns: a list of credentials + :rtype: list of :class:`keystoneclient.v3.credentials.Credential` - :param credential: Credential to update - :type credential: :class:`Credential` or str - :param user: User - :type user: :class:`keystoneclient.v3.users.User` or str - :param str type: credential type, should be either ``ec2`` or ``cert`` - :param JSON blob: Credential data - :param JSON data: Deprecated, use blob instead. - :param project: Project - :type project: :class:`keystoneclient.v3.projects.Project` or str - :param kwargs: Extra attributes passed to create. + """ + return super(CredentialManager, self).list(**kwargs) - :raises ValueError: if one of ``blob`` or ``data`` is not specified. + 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, - blob=self._get_data_blob(blob, data), + blob=blob, project_id=base.getid(project), **kwargs) def delete(self, credential): - """Delete a credential + """Delete a credential. - :param credential: Credential - :type credential: :class:`Credential` or str + :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 4439f4125..0f542b8b0 100644 --- a/keystoneclient/v3/domains.py +++ b/keystoneclient/v3/domains.py @@ -15,7 +15,6 @@ # under the License. from keystoneclient import base -from keystoneclient import utils class Domain(base.Resource): @@ -23,19 +22,35 @@ 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' - @utils.positional(1, enforcement=utils.positional.WARN) 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, @@ -43,14 +58,27 @@ def create(self, name, description=None, enabled=True, **kwargs): **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 list(self, **kwargs): """List domains. - ``**kwargs`` allows filter criteria to be passed where + :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 @@ -58,9 +86,22 @@ def list(self, **kwargs): kwargs['enabled'] = 0 return super(DomainManager, self).list(**kwargs) - @utils.positional(enforcement=utils.positional.WARN) 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, @@ -69,5 +110,14 @@ def update(self, domain, name=None, **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 index dc378c930..d32fbcaa3 100644 --- a/keystoneclient/v3/ec2.py +++ b/keystoneclient/v3/ec2.py @@ -14,8 +14,19 @@ 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 @@ -24,34 +35,64 @@ class EC2Manager(base.ManagerWithFind): resource_class = EC2 def create(self, user_id, project_id): - """Create a new access/secret pair for the user/project pair. + """Create a new access/secret pair. - :rtype: object of type :class:`EC2` - """ + :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._create('/users/%s/credentials/OS-EC2' % user_id, - body={'tenant_id': project_id}, - response_key="credential") + return self._post('/users/%s/credentials/OS-EC2' % user_id, + body={'tenant_id': project_id}, + response_key="credential") - def list(self, user_id): - """Get a list of access/secret pairs for a user_id. + def get(self, user_id, access): + """Retrieve an access/secret pair for a given access key. - :rtype: list of :class:`EC2` - """ - return self._list("/users/%s/credentials/OS-EC2" % user_id, - response_key="credentials") + :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. - def get(self, user_id, access): - """Get the access/secret pair for a given access key. + :returns: the specified access/secret pair returned from server. + :rtype: :class:`keystoneclient.v3.ec2.EC2` - :rtype: object of type :class:`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 for a user.""" + """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 eeddc4cd3..0452394a5 100644 --- a/keystoneclient/v3/endpoints.py +++ b/keystoneclient/v3/endpoints.py @@ -17,7 +17,6 @@ from keystoneclient import base from keystoneclient import exceptions from keystoneclient.i18n import _ -from keystoneclient import utils VALID_INTERFACES = ['public', 'admin', 'internal'] @@ -32,14 +31,17 @@ 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' @@ -47,12 +49,29 @@ class EndpointManager(base.CrudManager): 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) + msg %= ', '.join(VALID_INTERFACES) raise exceptions.ValidationError(msg) - @utils.positional(1, enforcement=utils.positional.WARN) 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` + + """ self._validate_interface(interface) return super(EndpointManager, self).create( service_id=base.getid(service), @@ -63,28 +82,70 @@ def create(self, service, url, interface=None, region=None, enabled=True, **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)) - @utils.positional(enforcement=utils.positional.WARN) def list(self, service=None, interface=None, region=None, enabled=None, - **kwargs): + region_id=None, **kwargs): """List endpoints. - If ``**kwargs`` are provided, then filter endpoints with - attributes matching ``**kwargs``. + :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, + region_id=region_id or base.getid(region), enabled=enabled, **kwargs) - @utils.positional(enforcement=utils.positional.WARN) 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. + + :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), @@ -96,5 +157,14 @@ def update(self, endpoint, service=None, url=None, interface=None, **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 a998323fa..843ad0028 100644 --- a/keystoneclient/v3/groups.py +++ b/keystoneclient/v3/groups.py @@ -15,7 +15,6 @@ # under the License. from keystoneclient import base -from keystoneclient import utils class Group(base.Resource): @@ -27,7 +26,7 @@ class Group(base.Resource): * description: group description """ - @utils.positional(enforcement=utils.positional.WARN) + def update(self, name=None, description=None): kwargs = { 'name': name if name is not None else self.name, @@ -47,27 +46,43 @@ 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' - @utils.positional(1, enforcement=utils.positional.WARN) 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, **kwargs) - @utils.positional(enforcement=utils.positional.WARN) def list(self, user=None, domain=None, **kwargs): """List groups. - If domain or user is provided, then filter groups with - that attribute. + :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 ``**kwargs`` are provided, then filter groups with - attributes matching ``**kwargs``. """ if user: base_url = '/users/%s' % base.getid(user) @@ -79,11 +94,31 @@ def list(self, user=None, domain=None, **kwargs): **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)) - @utils.positional(enforcement=utils.positional.WARN) 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, @@ -91,5 +126,14 @@ def update(self, group, name=None, description=None, **kwargs): **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 78cad9376..32de94e91 100644 --- a/keystoneclient/v3/policies.py +++ b/keystoneclient/v3/policies.py @@ -15,7 +15,6 @@ # under the License. from keystoneclient import base -from keystoneclient import utils class Policy(base.Resource): @@ -24,10 +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 """ - @utils.positional(enforcement=utils.positional.WARN) + def update(self, blob=None, type=None): kwargs = { 'blob': blob if blob is not None else self.blob, @@ -45,37 +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' - @utils.positional(1, enforcement=utils.positional.WARN) 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, **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, **kwargs): """List policies. - ``**kwargs`` allows filter criteria to be passed where - supported by the server. + :param kwargs: allows filter criteria to be passed where + supported by the server. + + :returns: a list of policies. + :rtype: list of :class:`keystoneclient.v3.policies.Policy`. + """ return super(PolicyManager, self).list(**kwargs) - @utils.positional(enforcement=utils.positional.WARN) - def update(self, entity, blob=None, type=None, **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, **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 5daaee55c..4ba94bf03 100644 --- a/keystoneclient/v3/projects.py +++ b/keystoneclient/v3/projects.py @@ -14,10 +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 _ -from keystoneclient import utils class Project(base.Resource): @@ -35,7 +36,7 @@ class Project(base.Resource): project in the hierarchy """ - @utils.positional(enforcement=utils.positional.WARN) + def update(self, name=None, description=None, enabled=None): kwargs = { 'name': name if name is not None else self.name, @@ -53,27 +54,50 @@ 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' - @utils.positional(1, enforcement=utils.positional.WARN) def create(self, name, domain, description=None, enabled=True, parent=None, **kwargs): """Create a project. - :param str name: project name. - :param domain: the project domain. - :type domain: :py:class:`keystoneclient.v3.domains.Domain` or str - :param str description: the project description. (optional) - :param boolean enabled: if the project is enabled. (optional) - :param parent: the project's parent in the hierarchy. (optional) - :type parent: :py:class:`keystoneclient.v3.projects.Project` or str - """ + :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, @@ -88,23 +112,50 @@ def create(self, name, domain, description=None, enabled=enabled, **kwargs) - @utils.positional(enforcement=utils.positional.WARN) - def list(self, domain=None, user=None, **kwargs): + def list(self, domain=None, user=None, parent=None, **kwargs): """List projects. - If domain or user are provided, then filter projects with - those attributes. + :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` - If ``**kwargs`` are provided, then filter projects with - attributes matching ``**kwargs``. """ 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), + parent_id=base.getid(parent), fallback_to_auth=True, **kwargs) + 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: @@ -119,29 +170,33 @@ def _check_not_subtree_as_ids_and_subtree_as_list(self, subtree_as_ids, 'parameters, not both') raise exceptions.ValidationError(msg) - @utils.positional() def get(self, project, subtree_as_list=False, parents_as_list=False, subtree_as_ids=False, parents_as_ids=False): - """Get a project. - - :param project: project to be retrieved. - :type project: :py:class:`keystoneclient.v3.projects.Project` or str - :param boolean subtree_as_list: retrieve projects below this project - in the hierarchy as a flat list. - (optional) - :param boolean parents_as_list: retrieve projects above this project - in the hierarchy as a flat list. - (optional) - :param boolean subtree_as_ids: retrieve the IDs from the projects below - this project in the hierarchy as a - structured dictionary. (optional) - :param boolean parents_as_ids: retrieve the IDs from the projects above - this project in the hierarchy as a - structured dictionary. (optional) + """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) @@ -162,11 +217,32 @@ def get(self, project, subtree_as_list=False, parents_as_list=False, 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) - return self._get(url + query, self.key) + 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 - @utils.positional(enforcement=utils.positional.WARN) def update(self, project, name=None, domain=None, description=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), @@ -176,5 +252,84 @@ def update(self, project, name=None, domain=None, description=None, **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 index 65cc6abc5..0538a6656 100644 --- a/keystoneclient/v3/regions.py +++ b/keystoneclient/v3/regions.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from debtcollector import removals from keystoneclient import base @@ -18,34 +19,43 @@ class Region(base.Resource): Attributes: * id: a string that identifies the region. - * description: a string that describes the region. Optional. - * parent_region_id: string that is the id field for an pre-existing - region in the backend. Allows for hierarchical region - organization + * 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. - Defaults to True """ + pass class RegionManager(base.CrudManager): - """Manager class for manipulating Identity endpoints.""" + """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 Catalog region. + """Create a region. - :param id: a string that identifies the region. If not specified - a unique identifier will be assigned to the region. - :param description: a string that describes the region. - :param parent_region: string that is the id field for a - pre-existing region in the backend. Allows for hierarchical - region organization. - :param enabled: determines whether the endpoint appears in the - catalog. + :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( @@ -53,29 +63,50 @@ def create(self, id=None, description=None, enabled=True, 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. - If ``**kwargs`` are provided, then filter regions with - attributes matching ``**kwargs``. + :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 Catalog region. + """Update a region. - :param region: a string that identifies the region. - :param description: a string that describes the region. - :param parent_region: string that is the id field for a - pre-existing region in the backend. Allows for hierarchical - region organization. - :param enabled: determines whether the endpoint appears in the - catalog. + :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( @@ -86,5 +117,14 @@ def update(self, region, description=None, enabled=None, **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 index d71f9eb11..ce1e550a0 100644 --- a/keystoneclient/v3/role_assignments.py +++ b/keystoneclient/v3/role_assignments.py @@ -16,7 +16,6 @@ class RoleAssignment(base.Resource): - """Represents an Identity role assignment. Attributes: @@ -26,12 +25,13 @@ class RoleAssignment(base.Resource): * 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' @@ -46,9 +46,26 @@ def _check_not_domain_and_project(self, domain, project): msg = _('Specify either a domain or project, not both') raise exceptions.ValidationError(msg) - def list(self, user=None, group=None, project=None, domain=None, role=None, - effective=False, os_inherit_extension_inherited_to=None): - """Lists role assignments. + 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. @@ -63,16 +80,23 @@ def list(self, user=None, group=None, project=None, domain=None, role=None, (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: @@ -83,13 +107,19 @@ def list(self, user=None, group=None, project=None, domain=None, role=None, 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) diff --git a/keystoneclient/v3/roles.py b/keystoneclient/v3/roles.py index ce72d70c5..3364bd88b 100644 --- a/keystoneclient/v3/roles.py +++ b/keystoneclient/v3/roles.py @@ -14,10 +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 _ -from keystoneclient import utils class Role(base.Resource): @@ -26,18 +27,34 @@ 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 @@ -49,9 +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' - - if use_inherit_extension: - base_url = '/OS-INHERIT' + base_url + 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) @@ -62,13 +91,26 @@ 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: - msg = _('Specify either a domain or project, not both') - raise exceptions.ValidationError(msg) - elif not (domain or project): - msg = _('Must specify either a domain or project') - 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: @@ -78,121 +120,452 @@ def _require_user_xor_group(self, user, group): msg = _('Must specify either a user or group') raise exceptions.ValidationError(msg) - @utils.positional(1, enforcement=utils.positional.WARN) - def create(self, name, **kwargs): + 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, + domain_id=domain_id, **kwargs) def get(self, role): - return super(RoleManager, self).get( - role_id=base.getid(role)) + """Retrieve a role. - @utils.positional(enforcement=utils.positional.WARN) - def list(self, user=None, group=None, domain=None, - project=None, os_inherit_extension_inherited=False, **kwargs): - """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. And if - ``**kwargs`` are provided, then also filter roles with - attributes matching ``**kwargs``. - - If 'os_inherit_extension_inherited' is passed, then OS-INHERIT will be - used. It provides the ability for projects to inherit role assignments - from their domains or from projects in the hierarchy. """ + 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, domain, project, os_inherit_extension_inherited) + user, group, system, domain, project, + os_inherit_extension_inherited + ) return super(RoleManager, self).list(base_url=base_url, **kwargs) return super(RoleManager, self).list(**kwargs) - @utils.positional(enforcement=utils.positional.WARN) def update(self, role, name=None, **kwargs): + """Update a role. + + :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, **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)) - @utils.positional(enforcement=utils.positional.WARN) - def grant(self, role, user=None, group=None, domain=None, project=None, - os_inherit_extension_inherited=False, **kwargs): - """Grants a role to a user or group on a domain or 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` - If 'os_inherit_extension_inherited' is passed, then OS-INHERIT will be - used. It provides the ability for projects to inherit role assignments - from their domains or from projects in the hierarchy. """ - self._require_domain_xor_project(domain, project) + 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, domain, project, os_inherit_extension_inherited) + user, group, system, domain, project, + os_inherit_extension_inherited) return super(RoleManager, self).put(base_url=base_url, role_id=base.getid(role), **kwargs) - @utils.positional(enforcement=utils.positional.WARN) - def check(self, role, user=None, group=None, domain=None, project=None, - os_inherit_extension_inherited=False, **kwargs): - """Checks if a user or group has a role on a domain or project. + 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` + + :returns: Response object with 204 status if specified role + doesn't exist. + :rtype: :class:`requests.models.Response` - If 'os_inherit_extension_inherited' is passed, then OS-INHERIT will be - used. It provides the ability for projects to inherit role assignments - from their domains or from projects in the hierarchy. """ - self._require_domain_xor_project(domain, project) + 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, domain, project, os_inherit_extension_inherited) + user, group, system, domain, project, + os_inherit_extension_inherited) return super(RoleManager, self).head( base_url=base_url, role_id=base.getid(role), os_inherit_extension_inherited=os_inherit_extension_inherited, **kwargs) - @utils.positional(enforcement=utils.positional.WARN) - def revoke(self, role, user=None, group=None, domain=None, project=None, - os_inherit_extension_inherited=False, **kwargs): - """Revokes a role from a user or group on a domain or project. + 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. + + :returns: the revoked role returned from server. + :rtype: list of :class:`keystoneclient.v3.roles.Role` - If 'os_inherit_extension_inherited' is passed, then OS-INHERIT will be - used. It provides the ability for projects to inherit role assignments - from their domains or from projects in the hierarchy. """ - self._require_domain_xor_project(domain, project) + 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, domain, project, os_inherit_extension_inherited) + user, group, system, domain, project, + os_inherit_extension_inherited) return super(RoleManager, self).delete( 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 d6d4c80db..631940e80 100644 --- a/keystoneclient/v3/services.py +++ b/keystoneclient/v3/services.py @@ -15,7 +15,6 @@ # under the License. from keystoneclient import base -from keystoneclient import utils class Service(base.Resource): @@ -23,51 +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' - @utils.positional(1, enforcement=utils.positional.WARN) - def create(self, name, type, enabled=True, description=None, **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)) - @utils.positional(enforcement=utils.positional.WARN) 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, + type=type_arg, **kwargs) - @utils.positional(enforcement=utils.positional.WARN) 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/apiclient/exceptions.py b/keystoneclient/v3/system.py similarity index 53% rename from keystoneclient/apiclient/exceptions.py rename to keystoneclient/v3/system.py index 982820830..8d3edafdd 100644 --- a/keystoneclient/apiclient/exceptions.py +++ b/keystoneclient/v3/system.py @@ -1,7 +1,4 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 Nebula, Inc. -# Copyright 2013 Alessio Ababilov -# Copyright 2013 OpenStack Foundation +# Copyright 2021 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -16,18 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. -""" -Exception definitions. +from keystoneclient import base -Deprecated since v0.7.1. Use 'keystoneclient.exceptions' instead of -this module. -""" -import warnings +class System(base.Resource): + """Represents the deployment system, with all the services in it. -from keystoneclient.exceptions import * # noqa + Attributes: + * all: boolean + """ - -warnings.warn("The 'keystoneclient.apiclient.exceptions' module is deprecated " - "since v.0.7.1. Use 'keystoneclient.exceptions' instead of this " - "module.", DeprecationWarning) + pass diff --git a/keystoneclient/v3/tokens.py b/keystoneclient/v3/tokens.py index 77edbc073..7e0cb07a8 100644 --- a/keystoneclient/v3/tokens.py +++ b/keystoneclient/v3/tokens.py @@ -12,7 +12,6 @@ from keystoneclient import access from keystoneclient import base -from keystoneclient import utils def _calc_id(token): @@ -31,48 +30,92 @@ def __init__(self, client): def revoke_token(self, token): """Revoke a token. - :param token: Token to be revoked. This can be an instance of - :py:class:`keystoneclient.access.AccessInfo` or a string - token_id. - """ + :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): + def get_revoked(self, audit_id_only=False): """Get revoked tokens list. - :returns: A dict containing "signed" which is a CMS formatted string. + :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 """ - - resp, body = self._client.get('/auth/tokens/OS-PKI/revoked') + path = '/auth/tokens/OS-PKI/revoked' + if audit_id_only: + path += '?audit_id_only' + resp, body = self._client.get(path) return body - @utils.positional.method(1) - def validate(self, token, include_catalog=True): - """Validate a token. + 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 - :param token: Token to be validated. This can be an instance of - :py:class:`keystoneclient.access.AccessInfo` or a string - token_id. - :param include_catalog: If False, the response is requested to not - include the catalog. - - :rtype: :py:class:`keystoneclient.access.AccessInfoV3` + :rtype: dict """ - - token_id = _calc_id(token) - headers = {'X-Subject-Token': token_id} + 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: - url += '?nocatalog' + 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 - access_info = access.AccessInfo.factory(resp=resp, body=body) - return access_info + 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 2e20ede3c..90cd51c45 100644 --- a/keystoneclient/v3/users.py +++ b/keystoneclient/v3/users.py @@ -14,14 +14,11 @@ # License for the specific language governing permissions and limitations # under the License. -import logging +from debtcollector import renames from keystoneclient import base from keystoneclient import exceptions -from keystoneclient.i18n import _, _LW -from keystoneclient import utils - -LOG = logging.getLogger(__name__) +from keystoneclient.i18n import _ class User(base.Resource): @@ -31,11 +28,13 @@ 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' @@ -45,22 +44,41 @@ def _require_user_and_group(self, user, group): msg = _('Specify both a user and a group') raise exceptions.ValidationError(msg) - @utils.positional(1, enforcement=utils.positional.WARN) + @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, 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, use default_project instead. + 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. - If both default_project and project is provided, the default_project - will be used. """ - if project: - LOG.warning(_LW("The project argument is deprecated, " - "use default_project instead.")) default_project_id = base.getid(default_project) or base.getid(project) user_data = base.filter_none(name=name, domain_id=base.getid(domain), @@ -71,30 +89,40 @@ def create(self, name, domain=None, project=None, password=None, enabled=enabled, **kwargs) - return self._create('/users', {'user': user_data}, 'user', - log=not bool(password)) + return self._post('/users', {'user': user_data}, 'user', + log=not bool(password)) - @utils.positional(enforcement=utils.positional.WARN) + @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. - If project, domain or group are provided, then filter - users with those attributes. - - If ``**kwargs`` are provided, then filter users with - attributes matching ``**kwargs``. + :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. + + :returns: a list of users. + :rtype: list of :class:`keystoneclient.v3.users.User`. .. warning:: - The project argument is deprecated, use default_project instead. + 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. - If both default_project and project is provided, the default_project - will be used. """ - if project: - LOG.warning(_LW("The project argument is deprecated, " - "use default_project instead.")) default_project_id = base.getid(default_project) or base.getid(project) if group: base_url = '/groups/%s' % base.getid(group) @@ -108,25 +136,54 @@ def list(self, project=None, domain=None, group=None, default_project=None, **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)) - @utils.positional(enforcement=utils.positional.WARN) + @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, 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, use default_project instead. + 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. - If both default_project and project is provided, the default_project - will be used. """ - if project: - LOG.warning(_LW("The project argument is deprecated, " - "use default_project instead.")) default_project_id = base.getid(default_project) or base.getid(project) user_data = base.filter_none(name=name, domain_id=base.getid(domain), @@ -144,7 +201,15 @@ def update(self, user, name=None, domain=None, project=None, password=None, log=False) def update_password(self, old_password, new_password): - """Update the password for the user the token belongs to.""" + """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) @@ -156,12 +221,22 @@ def update_password(self, old_password, new_password): params = {'user': {'password': new_password, 'original_password': old_password}} - base_url = '/users/%s/password' % self.api.user_id + base_url = '/users/%s/password' % self.client.user_id - return self._update(base_url, params, method='POST', log=False, - endpoint_filter={'interface': 'public'}) + 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) @@ -170,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) @@ -178,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) @@ -186,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 10f7b9b10..000000000 --- a/openstack-common.conf +++ /dev/null @@ -1,9 +0,0 @@ -[DEFAULT] - -# The list of modules to copy from oslo-incubator -module=apiclient -module=install_venv_common -module=memorycache - -# 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/keystoneclient/contrib/revoke/__init__.py b/releasenotes/notes/.placeholder similarity index 100% rename from keystoneclient/contrib/revoke/__init__.py rename to releasenotes/notes/.placeholder 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/keystoneclient/hacking/__init__.py b/releasenotes/source/_static/.placeholder similarity index 100% rename from keystoneclient/hacking/__init__.py rename to releasenotes/source/_static/.placeholder diff --git a/keystoneclient/middleware/__init__.py b/releasenotes/source/_templates/.placeholder similarity index 100% rename from keystoneclient/middleware/__init__.py rename to releasenotes/source/_templates/.placeholder 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 index 72324c29c..1b69b9833 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,14 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. +# 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 -pbr>=0.11,<2.0 - -argparse -Babel>=1.3 -iso8601>=0.1.9 -netaddr>=0.7.12 -oslo.config>=1.11.0 # Apache-2.0 -oslo.i18n>=1.5.0 # Apache-2.0 -oslo.serialization>=1.4.0 # Apache-2.0 -oslo.utils>=1.4.0 # Apache-2.0 -PrettyTable>=0.7,<0.8 -requests>=2.5.2 -six>=1.9.0 -stevedore>=1.3.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 30f415067..000000000 --- a/run_tests.sh +++ /dev/null @@ -1,187 +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 " -u, --update Update the virtual environment with any newer package versions" - echo " -p, --pep8 Just run flake8" - echo " -P, --no-pep8 Don't run flake8" - echo " -c, --coverage Generate coverage report" - echo " -d, --debug Run tests with testtools instead of testr. This allows you to use the debugger." - 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;; - -u|--update) update=1;; - -p|--pep8) just_flake8=1;; - -P|--no-pep8) no_flake8=1;; - -c|--coverage) coverage=1;; - -d|--debug) debug=1;; - -*) testropts="$testropts $1";; - *) testrargs="$testrargs $1" - esac -} - -venv=.venv -with_venv=tools/with_venv.sh -always_venv=0 -never_venv=0 -force=0 -no_site_packages=0 -installvenvopts= -testrargs= -testropts= -wrapper="" -just_flake8=0 -no_flake8=0 -coverage=0 -debug=0 -update=0 - -LANG=en_US.UTF-8 -LANGUAGE=en_US:en -LC_ALL=C -OS_STDOUT_NOCAPTURE=False -OS_STDERR_NOCAPTURE=False - -for arg in "$@"; do - process_option $arg -done - -if [ $no_site_packages -eq 1 ]; then - installvenvopts="--no-site-packages" -fi - - -function run_tests { - # Cleanup *.pyc - ${wrapper} find . -type f -name "*.pyc" -delete - - if [ $debug -eq 1 ]; then - if [ "$testropts" = "" ] && [ "$testrargs" = "" ]; then - # Default to running all tests if specific test is not - # provided. - testrargs="discover ./keystoneclient/tests/unit" - fi - ${wrapper} python -m testtools.run $testropts $testrargs - - # Short circuit because all of the testr and coverage stuff - # below does not make sense when running testtools.run for - # debugging purposes. - return $? - fi - - if [ $coverage -eq 1 ]; then - TESTRTESTS="$TESTRTESTS --coverage" - else - TESTRTESTS="$TESTRTESTS" - fi - - # Just run the test suites in current environment - set +e - testrargs=`echo "$testrargs" | sed -e's/^\s*\(.*\)\s*$/\1/'` - TESTRTESTS="$TESTRTESTS --testr-args='$testropts $testrargs'" - echo "Running \`${wrapper} $TESTRTESTS\`" - bash -c "${wrapper} $TESTRTESTS" - RESULT=$? - set -e - - copy_subunit_log - - if [ $coverage -eq 1 ]; then - echo "Generating coverage report in covhtml/" - # Don't compute coverage for common code, which is tested elsewhere - ${wrapper} coverage combine - ${wrapper} coverage html -d covhtml -i - fi - - return $RESULT -} - -function copy_subunit_log { - LOGNAME=`cat .testrepository/next-stream` - LOGNAME=$(($LOGNAME - 1)) - LOGNAME=".testrepository/${LOGNAME}" - cp $LOGNAME subunit.log -} - -function run_flake8 { - echo "Running flake8 ..." - srcfiles="keystoneclient" - # Just run Flake8 in current environment - ${wrapper} flake8 ${srcfiles} -} - -TESTRTESTS="python setup.py testr" - -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 [ $update -eq 1 ]; then - echo "Updating virtualenv..." - python tools/install_venv.py - 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_flake8 -eq 1 ]; then - run_flake8 - exit -fi - -run_tests - -# NOTE(sirp): we only want to run flake8 when we're running the full-test suite, -# not when we're running tests individually. To handle this, we need to -# distinguish between options (testropts), which begin with a '-', and -# arguments (testrargs). -if [ -z "$testrargs" ]; then - if [ $no_flake8 -eq 0 ]; then - run_flake8 - fi -fi diff --git a/setup.cfg b/setup.cfg index ae374ef8b..037fe1c57 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,11 +1,12 @@ [metadata] name = python-keystoneclient summary = Client Library for OpenStack Identity -description-file = +description_file = README.rst author = OpenStack -author-email = openstack-dev@lists.openstack.org -home-page = http://www.openstack.org/ +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 @@ -13,59 +14,25 @@ classifier = License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 - Programming Language :: Python :: 2.6 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.3 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 [files] packages = keystoneclient [entry_points] -console_scripts = - keystone = keystoneclient.shell:main - 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 - -[build_sphinx] -source-dir = doc/source -build-dir = doc/build -all_files = 1 - -[pbr] -warnerrors = True -autodoc_tree_index_modules = True -autodoc_tree_excludes = - setup.py - keystoneclient/tests/ - -[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 - -[wheel] -universal = 1 diff --git a/setup.py b/setup.py index 736375744..cd35c3c35 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,17 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools -# In python < 2.7.4, a lazy loading of package `pbr` will break -# setuptools if some other modules registered functions in `atexit`. -# solution from: http://bugs.python.org/issue15881#msg170215 -try: - import multiprocessing # noqa -except ImportError: - pass - setuptools.setup( - setup_requires=['pbr'], + setup_requires=['pbr>=2.0.0'], pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt index e51b439f6..238690c65 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,24 +1,18 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. +hacking>=6.1.0,<6.2.0 # Apache-2.0 -hacking>=0.10.0,<0.11 +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 -coverage>=3.6 -discover -fixtures>=0.3.14 -keyring>=2.1,!=3.3 -lxml>=2.3 -mock>=1.0 -mox3>=0.7.0 -oauthlib>=0.6 -oslosphinx>=2.5.0 # Apache-2.0 -oslotest>=1.5.1 # Apache-2.0 -pycrypto>=2.6 -requests-mock>=0.6.0 # Apache-2.0 -sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 -tempest-lib>=0.5.0 -testrepository>=0.0.18 -testresources>=0.2.4 -testtools>=0.9.36,!=1.2.0 -WebOb>=1.2.3 +# Bandit security code scanner +bandit>=1.1.0 # Apache-2.0 diff --git a/tools/install_venv.py b/tools/install_venv.py deleted file mode 100644 index 511e6dd74..000000000 --- a/tools/install_venv.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2010 OpenStack Foundation -# Copyright 2013 IBM Corp. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os -import sys - -import install_venv_common as install_venv # noqa - - -def print_help(venv, root): - help = """ - Openstack development environment setup is complete. - - Openstack development uses virtualenv to track and manage Python - dependencies while in development and testing. - - To activate the Openstack virtualenv for the extent of your current shell - session you can run: - - $ source %s/bin/activate - - Or, if you prefer, you can run commands in the virtualenv on a case by case - basis by running: - - $ %s/tools/with_venv.sh - - Also, make test will automatically use the virtualenv. - """ - print(help % (venv, root)) - - -def main(argv): - root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - - if os.environ.get('tools_path'): - root = os.environ['tools_path'] - venv = os.path.join(root, '.venv') - if os.environ.get('venv'): - venv = os.environ['venv'] - - pip_requires = os.path.join(root, 'requirements.txt') - test_requires = os.path.join(root, 'test-requirements.txt') - py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) - project = 'python-keystoneclient' - install = install_venv.InstallVenv(root, venv, pip_requires, test_requires, - py_version, project) - options = install.parse_args(argv) - install.check_python_version() - install.check_dependencies() - install.create_virtualenv(no_site_packages=options.no_site_packages) - install.install_dependencies() - print_help(venv, root) - -if __name__ == '__main__': - main(sys.argv) diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py deleted file mode 100644 index e279159ab..000000000 --- a/tools/install_venv_common.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# Copyright 2013 IBM Corp. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Provides methods needed by installation script for OpenStack development -virtual environments. - -Since this script is used to bootstrap a virtualenv from the system's Python -environment, it should be kept strictly compatible with Python 2.6. - -Synced in from openstack-common -""" - -from __future__ import print_function - -import optparse -import os -import subprocess -import sys - - -class InstallVenv(object): - - def __init__(self, root, venv, requirements, - test_requirements, py_version, - project): - self.root = root - self.venv = venv - self.requirements = requirements - self.test_requirements = test_requirements - self.py_version = py_version - self.project = project - - def die(self, message, *args): - print(message % args, file=sys.stderr) - sys.exit(1) - - def check_python_version(self): - if sys.version_info < (2, 6): - self.die("Need Python Version >= 2.6") - - def run_command_with_code(self, cmd, redirect_output=True, - check_exit_code=True): - """Runs a command in an out-of-process shell. - - Returns the output of that command. Working directory is self.root. - """ - if redirect_output: - stdout = subprocess.PIPE - else: - stdout = None - - proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout) - output = proc.communicate()[0] - if check_exit_code and proc.returncode != 0: - self.die('Command "%s" failed.\n%s', ' '.join(cmd), output) - return (output, proc.returncode) - - def run_command(self, cmd, redirect_output=True, check_exit_code=True): - return self.run_command_with_code(cmd, redirect_output, - check_exit_code)[0] - - def get_distro(self): - if (os.path.exists('/etc/fedora-release') or - os.path.exists('/etc/redhat-release')): - return Fedora( - self.root, self.venv, self.requirements, - self.test_requirements, self.py_version, self.project) - else: - return Distro( - self.root, self.venv, self.requirements, - self.test_requirements, self.py_version, self.project) - - def check_dependencies(self): - self.get_distro().install_virtualenv() - - def create_virtualenv(self, no_site_packages=True): - """Creates the virtual environment and installs PIP. - - Creates the virtual environment and installs PIP only into the - virtual environment. - """ - if not os.path.isdir(self.venv): - print('Creating venv...', end=' ') - if no_site_packages: - self.run_command(['virtualenv', '-q', '--no-site-packages', - self.venv]) - else: - self.run_command(['virtualenv', '-q', self.venv]) - print('done.') - else: - print("venv already exists...") - pass - - def pip_install(self, *args): - self.run_command(['tools/with_venv.sh', - 'pip', 'install', '--upgrade'] + list(args), - redirect_output=False) - - def install_dependencies(self): - print('Installing dependencies with pip (this can take a while)...') - - # First things first, make sure our venv has the latest pip and - # setuptools and pbr - self.pip_install('pip>=1.4') - self.pip_install('setuptools') - self.pip_install('pbr') - - self.pip_install('-r', self.requirements, '-r', self.test_requirements) - - def parse_args(self, argv): - """Parses command-line arguments.""" - parser = optparse.OptionParser() - parser.add_option('-n', '--no-site-packages', - action='store_true', - help="Do not inherit packages from global Python " - "install.") - return parser.parse_args(argv[1:])[0] - - -class Distro(InstallVenv): - - def check_cmd(self, cmd): - return bool(self.run_command(['which', cmd], - check_exit_code=False).strip()) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if self.check_cmd('easy_install'): - print('Installing virtualenv via easy_install...', end=' ') - if self.run_command(['easy_install', 'virtualenv']): - print('Succeeded') - return - else: - print('Failed') - - self.die('ERROR: virtualenv not found.\n\n%s development' - ' requires virtualenv, please install it using your' - ' favorite package management tool' % self.project) - - -class Fedora(Distro): - """This covers all Fedora-based distributions. - - Includes: Fedora, RHEL, CentOS, Scientific Linux - """ - - def check_pkg(self, pkg): - return self.run_command_with_code(['rpm', '-q', pkg], - check_exit_code=False)[1] == 0 - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if not self.check_pkg('python-virtualenv'): - self.die("Please install 'python-virtualenv'.") - - super(Fedora, self).install_virtualenv() diff --git a/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/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 fef36b790..e69fc3559 100644 --- a/tox.ini +++ b/tox.ini @@ -1,50 +1,100 @@ [tox] -minversion = 1.6 +minversion = 3.18.0 skipsdist = True -envlist = py26,py27,py33,py34,pep8 +envlist = py3,pep8,releasenotes +ignore_basepython_conflict = True [testenv] usedevelop = True -install_command = pip install -U {opts} {packages} -setenv = VIRTUAL_ENV={envdir} - OS_STDOUT_NOCAPTURE=False +setenv = OS_STDOUT_NOCAPTURE=False OS_STDERR_NOCAPTURE=False -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt -commands = python setup.py testr --testr-args='{posargs}' +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 = flake8 + bandit -r keystoneclient -x tests -n5 + +[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] -commands = python setup.py testr --coverage --testr-args='{posargs}' - -[tox:jenkins] -downloadcache = ~/cache/pip +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 = OS_TEST_PATH=./keystoneclient/tests/functional +setenv = {[testenv]setenv} + OS_TEST_PATH=./keystoneclient/tests/functional passenv = OS_* [flake8] -# H405: multi line docstring summary not separated with an empty line -ignore = H405 +# 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,*openstack/common* +exclude = .venv,.tox,dist,doc,*egg,build [testenv:docs] -commands= - python setup.py build_sphinx +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 -local-check-factory = keystoneclient.hacking.checks.factory + +[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 +