├── doc ├── ext │ ├── __init__.py │ └── apidoc.py ├── .gitignore ├── source │ ├── images │ │ ├── audit.png │ │ ├── graphs_authComp.svg │ │ └── graphs_authCompDelegate.svg │ ├── installation.rst │ ├── index.rst │ └── audit.rst ├── requirements.txt └── Makefile ├── keystonemiddleware ├── __init__.py ├── _common │ ├── __init__.py │ └── config.py ├── echo │ ├── __init__.py │ ├── __main__.py │ └── service.py ├── tests │ ├── __init__.py │ └── unit │ │ ├── __init__.py │ │ ├── audit │ │ ├── __init__.py │ │ ├── test_logging_notifier.py │ │ ├── base.py │ │ └── test_audit_oslo_messaging.py │ │ ├── auth_token │ │ ├── __init__.py │ │ ├── base.py │ │ ├── test_memcache_crypt.py │ │ ├── test_auth.py │ │ ├── test_connection_pool.py │ │ ├── test_config.py │ │ ├── test_user_auth_plugin.py │ │ └── test_cache.py │ │ ├── test_entry_points.py │ │ ├── test_access_rules.py │ │ ├── test_fixtures.py │ │ ├── utils.py │ │ ├── test_opts.py │ │ └── test_ec2_token_middleware.py ├── auth_token │ ├── _base.py │ ├── _exceptions.py │ ├── _user_plugin.py │ ├── _memcache_crypt.py │ ├── _identity.py │ └── _auth.py ├── exceptions.py ├── opts.py ├── i18n.py ├── audit │ ├── _notifier.py │ └── __init__.py ├── locale │ ├── ko_KR │ │ └── LC_MESSAGES │ │ │ └── keystonemiddleware.po │ └── en_GB │ │ └── LC_MESSAGES │ │ └── keystonemiddleware.po ├── fixture.py ├── oauth2_token.py └── oauth2_mtls_token.py ├── releasenotes ├── notes │ ├── .placeholder │ ├── tls-support-via-oslo-cache-51d744dd8a3f6ba0.yaml │ ├── remove-py38-438c67d4d2da02a3.yaml │ ├── remove-py39-ffac30d683a53099.yaml │ ├── drop-python-3-6-and-3-7-c407d5898c5eafec.yaml │ ├── bug-1803940-9a39c66014763af0.yaml │ ├── drop-py-2-7-6655f421a9cac0a2.yaml │ ├── bug-1737115-fa3d41e3d3cd7177.yaml │ ├── remove_kwargs_to_fetch_token-20e3451ed192ab6a.yaml │ ├── fix-cache-data-corrupted-issue-d1bd546625690581.yaml │ ├── bug-1762362-3d092b15c7bab3a4.yaml │ ├── bug-1737119-4afe548d28fbf8bb.yaml │ ├── bug-1544840-a534127f8663e40f.yaml │ ├── x-is-admin-project-header-97f1882e209fe727.yaml │ ├── fix-audit-no-service-endpoint-ports-72b2009d631dcf19.yaml │ ├── bug-1800017-0e5a9b8f62b5ca60.yaml │ ├── bug-1789351-102e2e5119be38b4.yaml │ ├── bug-1809101-6b5088443d5970ba.yaml │ ├── s3token-remove-deprecated-opts-a58a7715b275a89c.yaml │ ├── enable-sasl-protocol-946149083f604216.yaml │ ├── removed-as-of-ussuri-4e1ea485ba8801c9.yaml │ ├── bug-1766731-3b29192cfeb77964.yaml │ ├── ec2-v2-removal-6a886210cbc9d3e9.yaml │ ├── bug_1540115-677cf5016bc46348.yaml │ ├── bp-enhance-oauth2-interoperability-b1a00f10887d33dd.yaml │ ├── bp-whitelist-extension-for-app-creds-badf088c8ad584bb.yaml │ ├── s3token_auth_uri-490c1287d90b9df7.yaml │ ├── bp-oauth2-client-credentials-ext-19a40c655ee43f57.yaml │ ├── bug-1583702-a4469dc1556878b9.yaml │ ├── bug-1782404-c4e37bbc83756a89.yaml │ ├── bp-support-oauth2-mtls-2d2686c9d5b1fe1f.yaml │ ├── bp-enhance-oauth2-interoperability-dd998d4e0eafed3c.yaml │ ├── bug-1583699-dba4fe6c057e2be5.yaml │ ├── ksm_4.1.0-3cd78446d8e63616.yaml │ ├── bug-1813739-80eae72371903119.yaml │ ├── delay_auth_instead_of_503-f9b46bf4fbc11455.yaml │ ├── rename-auth-uri-d223d883f5898aee.yaml │ ├── bug-1747655-6e563d9317bb0f13.yaml │ ├── interface-option-ed551d2a3162668d.yaml │ ├── bug-1583690-da67472d7afff0bf.yaml │ ├── authprotocol-accepts-oslo-config-config-a37212b60f58e154.yaml │ ├── change-default-identity-endpoint-fab39579255c31bb.yaml │ ├── bug-1490804-87c0ff8e764945c1.yaml │ ├── bug-1695038-2cbedcabf8ecc057.yaml │ ├── bug-1677308-a2fa7de67f21cd84.yaml │ ├── deprecate-caching-tokens-in-process-a412b0f1dea84cb9.yaml │ ├── bug-1649735-3c68f3243e474775.yaml │ ├── deprecate-eventlet-unsafe-memcacheclientpool-f8b4a6733513d73e.yaml │ └── allow-expired-5ddbabcffc5678af.yaml └── source │ ├── _static │ └── .placeholder │ ├── _templates │ └── .placeholder │ ├── unreleased.rst │ ├── zed.rst │ ├── train.rst │ ├── xena.rst │ ├── yoga.rst │ ├── 2023.2.rst │ ├── 2024.2.rst │ ├── 2025.1.rst │ ├── 2025.2.rst │ ├── ussuri.rst │ ├── 2023.1.rst │ ├── 2024.1.rst │ ├── ocata.rst │ ├── newton.rst │ ├── pike.rst │ ├── rocky.rst │ ├── stein.rst │ ├── wallaby.rst │ ├── queens.rst │ ├── victoria.rst │ ├── mitaka.rst │ ├── index.rst │ └── locale │ ├── fr │ └── LC_MESSAGES │ │ └── releasenotes.po │ └── ko_KR │ └── LC_MESSAGES │ └── releasenotes.po ├── pyproject.toml ├── .stestr.conf ├── .gitreview ├── .coveragerc ├── config-generator └── keystonemiddleware.conf ├── .zuul.yaml ├── test-requirements.txt ├── CONTRIBUTING.rst ├── requirements.txt ├── HACKING.rst ├── .gitignore ├── setup.py ├── README.rst ├── setup.cfg └── tox.ini /doc/ext/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /keystonemiddleware/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /releasenotes/notes/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /keystonemiddleware/_common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /keystonemiddleware/echo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | source/api/ 3 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /releasenotes/source/_static/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /releasenotes/source/_templates/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/audit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/auth_token/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pbr>=6.1.1"] 3 | build-backend = "pbr.build" 4 | -------------------------------------------------------------------------------- /.stestr.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | test_path=${OS_TEST_PATH:-./keystonemiddleware/tests/unit} 3 | top_dir=./ -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=review.opendev.org 3 | port=29418 4 | project=openstack/keystonemiddleware.git 5 | -------------------------------------------------------------------------------- /doc/source/images/audit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/keystonemiddleware/HEAD/doc/source/images/audit.png -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = keystonemiddleware 4 | omit = keystonemiddleware/tests/* 5 | 6 | [report] 7 | ignore_errors = True 8 | -------------------------------------------------------------------------------- /config-generator/keystonemiddleware.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | output_file = etc/keystone.conf.sample 3 | wrap_width = 79 4 | namespace = keystonemiddleware.auth_token 5 | -------------------------------------------------------------------------------- /releasenotes/source/unreleased.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Current Series Release Notes 3 | ============================== 4 | 5 | .. release-notes:: 6 | -------------------------------------------------------------------------------- /releasenotes/notes/tls-support-via-oslo-cache-51d744dd8a3f6ba0.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | other: 3 | - | 4 | Added tls support to the MemcacheClientPool driver via oslo.cache 5 | -------------------------------------------------------------------------------- /releasenotes/source/zed.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | Zed Series Release Notes 3 | ======================== 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/zed 7 | -------------------------------------------------------------------------------- /releasenotes/source/train.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Train Series Release Notes 3 | ========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/train 7 | -------------------------------------------------------------------------------- /releasenotes/source/xena.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Xena Series Release Notes 3 | ========================= 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/xena 7 | -------------------------------------------------------------------------------- /releasenotes/source/yoga.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Yoga Series Release Notes 3 | ========================= 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/yoga 7 | -------------------------------------------------------------------------------- /releasenotes/source/2023.2.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2023.2 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/2023.2 7 | -------------------------------------------------------------------------------- /releasenotes/source/2024.2.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2024.2 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/2024.2 7 | -------------------------------------------------------------------------------- /releasenotes/source/2025.1.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2025.1 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/2025.1 7 | -------------------------------------------------------------------------------- /releasenotes/source/2025.2.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2025.2 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/2025.2 7 | -------------------------------------------------------------------------------- /releasenotes/source/ussuri.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | Ussuri Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/ussuri 7 | -------------------------------------------------------------------------------- /releasenotes/notes/remove-py38-438c67d4d2da02a3.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Python 3.8 support was dropped. The minimum version of Python now supported 5 | is Python 3.9. 6 | -------------------------------------------------------------------------------- /releasenotes/notes/remove-py39-ffac30d683a53099.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Support for Python 3.9 has been removed. Now Python 3.10 is the minimum 5 | version supported. 6 | -------------------------------------------------------------------------------- /releasenotes/source/2023.1.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2023.1 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/2023.1 7 | -------------------------------------------------------------------------------- /releasenotes/source/2024.1.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2024.1 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/2024.1 7 | -------------------------------------------------------------------------------- /releasenotes/source/ocata.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | Ocata Series Release Notes 3 | ============================ 4 | 5 | .. release-notes:: 6 | :branch: origin/stable/ocata 7 | -------------------------------------------------------------------------------- /releasenotes/source/newton.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Newton Series Release Notes 3 | ============================= 4 | 5 | .. release-notes:: 6 | :branch: origin/stable/newton 7 | -------------------------------------------------------------------------------- /releasenotes/source/pike.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Pike Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: stable/pike 7 | -------------------------------------------------------------------------------- /releasenotes/source/rocky.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Rocky Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: stable/rocky 7 | -------------------------------------------------------------------------------- /releasenotes/source/stein.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Stein Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: stable/stein 7 | -------------------------------------------------------------------------------- /releasenotes/source/wallaby.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | Wallaby Series Release Notes 3 | ============================ 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/wallaby 7 | -------------------------------------------------------------------------------- /releasenotes/source/queens.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Queens Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: stable/queens 7 | -------------------------------------------------------------------------------- /releasenotes/source/victoria.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Victoria Series Release Notes 3 | ============================= 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/victoria 7 | -------------------------------------------------------------------------------- /releasenotes/source/mitaka.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Mitaka Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: origin/stable/mitaka 7 | -------------------------------------------------------------------------------- /releasenotes/notes/drop-python-3-6-and-3-7-c407d5898c5eafec.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Python 3.6 & 3.7 support has been dropped. The minimum version of Python now 5 | supported is Python 3.8. 6 | -------------------------------------------------------------------------------- /keystonemiddleware/echo/__main__.py: -------------------------------------------------------------------------------- 1 | from keystonemiddleware.echo import service 2 | 3 | 4 | try: 5 | service.EchoService() 6 | except KeyboardInterrupt: # nosec 7 | # The user wants this application to exit. 8 | pass 9 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1803940-9a39c66014763af0.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - > 4 | [`bug 1803940 `_] 5 | Request ID and global request ID have been added to CADF notifications. 6 | -------------------------------------------------------------------------------- /.zuul.yaml: -------------------------------------------------------------------------------- 1 | - project: 2 | templates: 3 | - openstack-cover-jobs 4 | - openstack-python3-jobs 5 | - publish-openstack-docs-pti 6 | - check-requirements 7 | - lib-forward-testing-python3 8 | - release-notes-jobs-python3 9 | -------------------------------------------------------------------------------- /releasenotes/notes/drop-py-2-7-6655f421a9cac0a2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Python 2.7 support has been dropped. Last release of keystonemiddleware 5 | to support python 2.7 is OpenStack Train. The minimum version of Python now 6 | supported is Python 3.6. -------------------------------------------------------------------------------- /releasenotes/notes/bug-1737115-fa3d41e3d3cd7177.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | [`bug 1737115 `_] 5 | Last release have accidentaly make python-memcached a hard dependency, this 6 | have changed back to an optional one. 7 | -------------------------------------------------------------------------------- /releasenotes/notes/remove_kwargs_to_fetch_token-20e3451ed192ab6a.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | other: 3 | - > 4 | The ``kwargs_to_fetch_token`` setting was removed from the 5 | ``BaseAuthProtocol`` class. Implementations of auth_token now assume kwargs 6 | will be passed to the ``fetch_token`` method. 7 | -------------------------------------------------------------------------------- /releasenotes/notes/fix-cache-data-corrupted-issue-d1bd546625690581.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | In situation of encryption using memcached. Its possible that data 5 | in memcached becomes un-decryptable. The previous implementation 6 | of token cache was not correctly handling the case. 7 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1762362-3d092b15c7bab3a4.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - > 4 | [`bug 1762362 `_] 5 | The value of the header "WWW-Authenticate" in a 401 (Unauthorized) response 6 | now is double quoted to follow the RFC requirement. 7 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1737119-4afe548d28fbf8bb.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - > 4 | [`bug 1737119 `_] 5 | If the application was not using the global cfg.CONF object, the configuration 6 | was not read from the configuration file. This have been fixed. 7 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1544840-a534127f8663e40f.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - > 4 | [`bug 1544840 `_] 5 | Adding audit middleware specific notification related configuration 6 | to allow a different notification driver and transport for audit if needed. 7 | -------------------------------------------------------------------------------- /releasenotes/notes/x-is-admin-project-header-97f1882e209fe727.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | prelude: > 3 | - Add the `X_IS_ADMIN_PROJECT` header. 4 | features: 5 | - Added the `X_IS_ADMIN_PROJECT` header to authenticated headers. This has 6 | the string value of 'True' or 'False' and can be used to enforce admin 7 | project policies. 8 | -------------------------------------------------------------------------------- /releasenotes/notes/fix-audit-no-service-endpoint-ports-72b2009d631dcf19.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | [`bug 1797584 `_] 5 | Fixed a bug where the audit code would select the wrong target service 6 | if the OpenStack service endpoints were not using unique TCP ports. 7 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1800017-0e5a9b8f62b5ca60.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | [`bug 1800017 `_] 5 | Fix audit middleware service catalog parsing for the scenario where a 6 | service does not contain any endpoints. In that case, we should just skip 7 | over that service. 8 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1789351-102e2e5119be38b4.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - > 4 | [`bug 1789351 `_] 5 | Fixed the bug that when initialize `AuthProtocol`, it'll raise "dictionary 6 | changed size during iteration" error if the input `CONF` object contains 7 | deprecated options. 8 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1809101-6b5088443d5970ba.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | [`bug 1809101 `_] 5 | Fix req.context of Keystone audit middleware and Glance conflict with each 6 | other issue. The audit middleware now stores the admin context to 7 | req.environ['audit.context']. 8 | -------------------------------------------------------------------------------- /releasenotes/notes/s3token-remove-deprecated-opts-a58a7715b275a89c.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | The s3token middleware now requires the ``www_authenticate_uri`` option and 5 | no longer loads the following deprecated options. 6 | 7 | - ``auth_uri`` 8 | - ``auth_host`` 9 | - ``auth_port`` 10 | - ``auth_protocol`` 11 | -------------------------------------------------------------------------------- /releasenotes/notes/enable-sasl-protocol-946149083f604216.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | Add the feature to support SASL for keystonemiddleware to improve 5 | the security of authority. 6 | memcache_sasl_enabled: enable the SASL option or not. 7 | memcache_username: the user name for the SASL 8 | memcache_password: the user password for SASL -------------------------------------------------------------------------------- /releasenotes/notes/removed-as-of-ussuri-4e1ea485ba8801c9.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | [`bug 1845539 `_] 5 | [`bug 1777177 `_] 6 | keystonemiddleware no longer supports the keystone v2.0 api, all 7 | associated functionality has been removed. 8 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1766731-3b29192cfeb77964.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | [`bug 1766731 `_] 5 | Keystonemiddleware now supports system scoped tokens. When a system-scoped 6 | token is parsed by auth_token middleware, it will set the 7 | ``OpenStack-System-Scope`` header accordingly. 8 | -------------------------------------------------------------------------------- /releasenotes/notes/ec2-v2-removal-6a886210cbc9d3e9.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | other: 3 | - | 4 | [`bug 1845539 `_] 5 | The ec2 'url' config option now defaults to 6 | https://localhost:5000/v3/ec2tokens with the removal of ec2 v2.0 support. 7 | Keystonemiddleware no longer supports ec2tokens using the v2.0 API. 8 | -------------------------------------------------------------------------------- /releasenotes/notes/bug_1540115-677cf5016bc46348.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - > 4 | [`bug 1540115 `_] 5 | Optional dependencies can now be installed using `extras`. To install audit 6 | related libraries, use ``pip install keystonemiddleware[audit_nofications]``. 7 | Refer to keystonemiddleware documentation for further information. 8 | -------------------------------------------------------------------------------- /releasenotes/notes/bp-enhance-oauth2-interoperability-b1a00f10887d33dd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | [`blueprint enhance-oauth2-interoperability `_] 5 | Added the ability to authenticate using a system-scoped token and the 6 | ability to authenticate using a cached token to the external_oauth2_token 7 | filter. 8 | -------------------------------------------------------------------------------- /releasenotes/notes/bp-whitelist-extension-for-app-creds-badf088c8ad584bb.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | [`spec `_] 5 | The auth_token middleware now has support for accepting or denying incoming 6 | requests based on access rules provided by users in their keystone 7 | application credentials. 8 | -------------------------------------------------------------------------------- /releasenotes/notes/s3token_auth_uri-490c1287d90b9df7.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - A new configuration option for the s3token middleware called auth_uri can 4 | be used to set the URI to be used for authentication. This replaces 5 | auth_host, auth_port, and auth_protocol. 6 | deprecations: 7 | - The auth_host, auth_port, and auth_protocol configuration options to the 8 | s3token middleware are now deprecated. 9 | -------------------------------------------------------------------------------- /releasenotes/notes/bp-oauth2-client-credentials-ext-19a40c655ee43f57.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | [`blueprint oauth2-client-credentials-ext `_] 5 | The oauth2_token filter has been added for accepting or denying incoming 6 | requests containing OAuth2.0 client credentials access tokens passed via 7 | the Authorization headers as bearer tokens. 8 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1583702-a4469dc1556878b9.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - > 4 | [`bug 1583702 `_] 5 | Some services such as Swift does not use Oslo (global) config. In that 6 | case, the options are conveyed via local config. This patch utilized 7 | an established pattern in auth_token middleware, which is to first 8 | look for the given option in local config, then Oslo global config. 9 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1782404-c4e37bbc83756a89.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - > 4 | [`bug 1782404 `_] 5 | Keystonemiddleware incorrectly implemented an abstraction for the memcache 6 | client pool that utilized a `queue.Queue` `get` method instead of the 7 | supplied `acquire()` context manager. The `acquire()` context manager 8 | properly places the client connection back into the pool after `__exit__`. 9 | -------------------------------------------------------------------------------- /releasenotes/notes/bp-support-oauth2-mtls-2d2686c9d5b1fe1f.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | [`blueprint support-oauth2-mtls `_] 5 | The oauth2_mtls_token filter has been added for accepting or denying 6 | incoming requests containing OAuth 2.0 certificate-bound access tokens 7 | that are obtained from keystone identity server by users through their 8 | OAuth 2.0 credentials and Mutual-TLS certificates. 9 | -------------------------------------------------------------------------------- /releasenotes/notes/bp-enhance-oauth2-interoperability-dd998d4e0eafed3c.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | [`blueprint enhance-oauth2-interoperability `_] 5 | The external_oauth2_token filter has been added for accepting or denying 6 | incoming requests containing OAuth 2.0 access tokens that are obtained 7 | from an external authorization server by users through their OAuth 2.0 8 | credentials. 9 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1583699-dba4fe6c057e2be5.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - > 4 | [`bug 1583699 `_] 5 | Some service APIs (such as Swift list public containers) do not require 6 | a token. Therefore, there will be no identity or service catalog 7 | information available. In these cases, audit now fills in the default 8 | (i.e. taxonomy.UNKNOWN) for both initiator and target instead of raising 9 | an exception. 10 | -------------------------------------------------------------------------------- /releasenotes/source/index.rst: -------------------------------------------------------------------------------- 1 | ================================== 2 | keystonemiddleware Release Notes 3 | ================================== 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | unreleased 9 | 2025.2 10 | 2025.1 11 | 2024.2 12 | 2024.1 13 | 2023.2 14 | 2023.1 15 | zed 16 | yoga 17 | xena 18 | wallaby 19 | victoria 20 | ussuri 21 | train 22 | stein 23 | rocky 24 | queens 25 | pike 26 | ocata 27 | newton 28 | mitaka 29 | -------------------------------------------------------------------------------- /releasenotes/notes/ksm_4.1.0-3cd78446d8e63616.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - > 4 | [`bug 1523311 `_] 5 | Do not list deprecated opts in sample config. 6 | - > 7 | [`bug 1333951 `_] 8 | Add support for parsing AWS v4 for ec2. 9 | - > 10 | [`bug 1423973 `_] 11 | Use oslo.config choices for config options. 12 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1813739-80eae72371903119.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | [`bug 1813739 `_] 5 | When admin identity endpoint is not created yet, keystonemiddleware emit 6 | EndpointNotFound exception. Even after admin identity endpoint created, 7 | auth_token middleware could not be notified of update since it does not 8 | invalidate existing auth. Add an invalidation step so that endpoint 9 | updates can be detected. 10 | -------------------------------------------------------------------------------- /releasenotes/notes/delay_auth_instead_of_503-f9b46bf4fbc11455.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | When ``delay_auth_decision`` is enabled and a Keystone failure prevents 5 | a final decision about whether a token is valid or invalid, it will be 6 | marked invalid and the application will be responsible for a final auth 7 | decision. This is similar to what happens when a token is confirmed *not* 8 | valid. This allows a Keystone outage to only affect Keystone users in a 9 | multi-auth system. 10 | -------------------------------------------------------------------------------- /releasenotes/notes/rename-auth-uri-d223d883f5898aee.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | deprecations: 3 | - | 4 | The auth_uri parameter of keystone_authtoken is deprecated in favor of 5 | www_authenticate_uri. The auth_uri option was often confused with the 6 | auth_url parameter of the keystoneauth plugin, which was also effectively 7 | always required. The parameter refers to the WWW-Authenticate header that is 8 | returned when the user needs to be redirected to the Identity service for 9 | authentication. 10 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1747655-6e563d9317bb0f13.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | [`bug/1747655 `_] 5 | When keystone is temporarily unavailable, keystonemiddleware correctly 6 | sends a 503 response to the HTTP client but was not identifying which 7 | service was down, leading to confusion on whether it was keystone or the 8 | service using keystonemiddleware that was unavailable. This change 9 | identifies keystone in the error response. 10 | -------------------------------------------------------------------------------- /releasenotes/notes/interface-option-ed551d2a3162668d.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | [`bug 1830002 `_] 5 | In order to allow an installation to work without deploying an admin 6 | Identity endpoint, a new option `interface` has been added, allowing 7 | select the Identity endpoint that is being used when verifying auth 8 | tokens. It defaults to `admin` in order to replicate the old behaviour, 9 | but may be set to `public` or `internal` as needed. 10 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1583690-da67472d7afff0bf.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - > 4 | [`bug 1583690 `_] 5 | For services such as Swift, which may not be utilizing oslo_config, we need 6 | to be able to determine the project name from local config. If project 7 | name is specified in both local config and oslo_config, the one in local 8 | config will be used instead. 9 | 10 | In case project is undetermined (i.e. not set), we use taxonomy.UNKNOWN as 11 | an indicator so operators can take corrective actions. 12 | -------------------------------------------------------------------------------- /releasenotes/notes/authprotocol-accepts-oslo-config-config-a37212b60f58e154.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - > 4 | [`bug 1540022 `_] 5 | The auth_token middleware will now accept a conf setting named 6 | ``oslo_config_config``. If this is set its value must be an existing 7 | oslo_config `ConfigOpts`. ``oslo_config_config`` takes precedence over 8 | ``oslo_config_project``. This feature is useful to applications that 9 | are instantiating the auth_token middleware themselves and wish to 10 | use an existing configuration. 11 | -------------------------------------------------------------------------------- /releasenotes/notes/change-default-identity-endpoint-fab39579255c31bb.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | prelude: > 3 | Since the removal of the Identity API v2 Keystone no longer has any 4 | special functionality that requires using the admin endpoint for it. So 5 | this release changes the default endpoint being used from ``admin`` to 6 | ``internal``, allowing deployments to work without an admin endpoint. 7 | upgrade: 8 | - | 9 | [`bug 1830002 `_] 10 | The default Identity endpoint has been changed from ``admin`` to 11 | ``internal``. 12 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | hacking~=6.1.0 # Apache-2.0 2 | 3 | coverage>=4.0 # Apache-2.0 4 | cryptography>=3.0 # BSD/Apache-2.0 5 | fixtures>=3.0.0 # Apache-2.0/BSD 6 | oslotest>=3.2.0 # Apache-2.0 7 | stevedore>=1.20.0 # Apache-2.0 8 | requests-mock>=1.2.0 # Apache-2.0 9 | stestr>=2.0.0 # Apache-2.0 10 | testresources>=2.0.0 # Apache-2.0/BSD 11 | testtools>=2.2.0 # MIT 12 | python-binary-memcached>=0.29.0 # MIT 13 | python-memcached>=1.59 # PSF 14 | WebTest>=2.0.27 # MIT 15 | oslo.messaging>=5.29.0 # Apache-2.0 16 | PyJWT>=2.4.0 # MIT 17 | 18 | # Bandit security code scanner 19 | bandit>=1.1.0 # Apache-2.0 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | If you would like to contribute to the development of OpenStack, 2 | you must follow the steps in this page: 3 | 4 | https://docs.openstack.org/infra/manual/developers.html 5 | 6 | Once those steps have been completed, changes to OpenStack 7 | should be submitted for review via the Gerrit tool, following 8 | the workflow documented at: 9 | 10 | https://docs.openstack.org/infra/manual/developers.html#development-workflow 11 | 12 | Pull requests submitted through GitHub will be ignored. 13 | 14 | Bugs should be filed on Launchpad, not GitHub: 15 | 16 | https://bugs.launchpad.net/keystonemiddleware 17 | -------------------------------------------------------------------------------- /keystonemiddleware/auth_token/_base.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | AUTHTOKEN_GROUP = 'keystone_authtoken' 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements lower bounds listed here are our best effort to keep them up to 2 | # date but we do not test them so no guarantee of having them all correct. If 3 | # you find any incorrect lower bounds, let us know or propose a fix. 4 | 5 | keystoneauth1>=3.12.0 # Apache-2.0 6 | oslo.cache>=3.11.0 # Apache-2.0 7 | oslo.config>=5.2.0 # Apache-2.0 8 | oslo.context>=2.19.2 # Apache-2.0 9 | oslo.i18n>=3.15.3 # Apache-2.0 10 | oslo.log>=3.36.0 # Apache-2.0 11 | oslo.serialization>=2.18.0 # Apache-2.0 12 | oslo.utils>=3.33.0 # Apache-2.0 13 | pbr>=2.0.0 # Apache-2.0 14 | pycadf>=1.1.0 # Apache-2.0 15 | PyJWT>=2.4.0 # MIT 16 | python-keystoneclient>=3.20.0 # Apache-2.0 17 | requests>=2.14.2 # Apache-2.0 18 | WebOb>=1.7.1 # MIT 19 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | # For generating sphinx documentation 2 | doc8>=0.6.0 # Apache-2.0 3 | openstackdocstheme>=2.2.1 # Apache-2.0 4 | reno>=3.1.0 # Apache-2.0 5 | sphinx>=2.0.0 # BSD 6 | sphinxcontrib-apidoc>=0.2.0 # BSD 7 | 8 | # PDF Docs 9 | sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD 10 | 11 | # For autodoc builds 12 | oslotest>=3.2.0 # Apache-2.0 13 | requests-mock>=1.2.0 # Apache-2.0 14 | testresources>=2.0.0 # Apache-2.0/BSD 15 | python-binary-memcached>=0.29.0 # MIT 16 | python-memcached>=1.56 # PSF 17 | WebTest>=2.0.27 # MIT 18 | oslo.messaging>=5.29.0 # Apache-2.0 19 | pycadf>=1.1.0 # Apache-2.0 20 | PyJWT>=2.4.0 # MIT 21 | keystoneauth1>=3.12.0 # Apache-2.0 22 | oslo.cache>=3.11.0 # Apache-2.0 23 | python-keystoneclient>=3.20.0 # Apache-2.0 24 | -------------------------------------------------------------------------------- /HACKING.rst: -------------------------------------------------------------------------------- 1 | Keystone Style Commandments 2 | =========================== 3 | 4 | - Step 1: Read the OpenStack Style Commandments 5 | https://docs.openstack.org/hacking/latest/ 6 | - Step 2: Read on 7 | 8 | Exceptions 9 | ---------- 10 | 11 | When dealing with exceptions from underlying libraries, translate those 12 | exceptions to an instance or subclass of ClientException. 13 | 14 | ======= 15 | Testing 16 | ======= 17 | 18 | Keystone Middleware uses testtools and testr for its unittest suite 19 | and its test runner. Basic workflow around our use of tox and testr can 20 | be found at https://wiki.openstack.org/testr. If you'd like to learn more 21 | in depth: 22 | 23 | https://testtools.readthedocs.org/ 24 | https://testrepository.readthedocs.org/ 25 | -------------------------------------------------------------------------------- /keystonemiddleware/exceptions.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | 14 | class KeystoneMiddlewareException(Exception): 15 | pass 16 | 17 | 18 | class ConfigurationError(KeystoneMiddlewareException): 19 | pass 20 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1490804-87c0ff8e764945c1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - > 4 | [`bug 1490804 `_] 5 | The auth_token middleware validates the token's audit IDs during offline 6 | token validation if the Identity server includes audit IDs in the token 7 | revocation list. 8 | security: 9 | - > 10 | [`bug 1490804 `_] 11 | [`CVE-2015-7546 `_] 12 | A bug is fixed where an attacker could avoid token revocation when the PKI 13 | or PKIZ token provider is used. The complete remediation for this 14 | vulnerability requires the corresponding fix in the Identity (keystone) 15 | project. 16 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1695038-2cbedcabf8ecc057.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - > 4 | [`bug 1695038 `_] 5 | The use_oslo_messaging configuration option is added for services such as 6 | Swift, which need the audit middleware to use the local logger instead of 7 | the oslo.messaging notifier regardless of whether the oslo.messaging package 8 | is present or not. 9 | Leave this option set to its default True value to keep the previous behavior 10 | unchanged - the audit middleware will use the oslo.messaging notifier if the 11 | oslo.messaging package is present, and the local logger otherwise. 12 | Services that rely on the local logger for audit notifications must set this 13 | option to False. 14 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1677308-a2fa7de67f21cd84.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | [`bug 1677308 `_] 5 | Removes ``pycrypto`` dependency as the library is unmaintained, and 6 | replaces it with the ``cryptography`` library. 7 | upgrade: 8 | - | 9 | [`bug 1677308 `_] 10 | There is no upgrade impact when switching from ``pycrypto`` to 11 | ``cryptography``. All data will be encrypted and decrypted using identical 12 | blocksize, padding, algorithm (AES) and mode (CBC). Data previously 13 | encrypted using ``pycrypto`` can be decrypted using both ``pycrypto`` and 14 | ``cryptography``. The same is true of data encrypted using 15 | ``cryptography``. 16 | -------------------------------------------------------------------------------- /doc/source/installation.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Installation 3 | ============== 4 | 5 | Install using pip 6 | ----------------- 7 | 8 | At the command line:: 9 | 10 | $ pip install keystonemiddleware 11 | 12 | Or, if you want to use it in a virtualenvwrapper:: 13 | 14 | $ mkvirtualenv keystonemiddleware 15 | $ pip install keystonemiddleware 16 | 17 | Install optional dependencies 18 | ----------------------------- 19 | 20 | Certain keystonemiddleware features are only available if specific libraries 21 | are available. These libraries can be installed using pip as well. 22 | 23 | To install support for audit notifications:: 24 | 25 | $ pip install keystonemiddleware[audit_notifications] 26 | 27 | To install support for memcache encryption:: 28 | 29 | $ pip install keystonemiddleware[memcache_encryption] 30 | -------------------------------------------------------------------------------- /keystonemiddleware/opts.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 OpenStack Foundation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | __all__ = ( 16 | 'list_auth_token_opts', 17 | ) 18 | 19 | from keystonemiddleware import auth_token 20 | 21 | 22 | def list_auth_token_opts(): 23 | return auth_token.list_opts() 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | .testrepository 29 | cover 30 | .stestr/ 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | 40 | # Complexity 41 | output/*.html 42 | output/*/index.html 43 | 44 | # Sphinx 45 | doc/build 46 | 47 | # pbr generates these 48 | AUTHORS 49 | ChangeLog 50 | 51 | # Editors 52 | *~ 53 | .*.swp 54 | 55 | # Oslo Sync 56 | .update-venv 57 | 58 | # Files created by releasenotes build 59 | releasenotes/build 60 | 61 | # sample config included in docs 62 | doc/source/_static/keystonemiddleware.conf.sample 63 | -------------------------------------------------------------------------------- /releasenotes/notes/deprecate-caching-tokens-in-process-a412b0f1dea84cb9.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | deprecations: 3 | - > 4 | With the release of 4.2.0 of keystonemiddleware we no longer recommend 5 | using the in-process token cache. In-process caching may result in 6 | inconsistent validation, poor UX and race conditions. 7 | 8 | It is recommended that the `memcached_servers` option is set in the 9 | `keystone_authtoken` configuration section of the various services (e.g. 10 | nova, glance, ...) with the endpoint of running memcached server(s). 11 | 12 | When the feature is removed, not setting the `memcached_servers` 13 | option will cause keystone to validate tokens more frequently, increasing 14 | load. In production, use of caching is highly recommended. 15 | 16 | This feature is deprecated as of 4.2.0 and is targeted for removal in 17 | keystonemiddleware 5.0.0 or in the `O` development cycle, whichever is 18 | later. 19 | -------------------------------------------------------------------------------- /keystonemiddleware/i18n.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 IBM Corp. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | """oslo.i18n integration module. 16 | 17 | See https://docs.openstack.org/oslo.i18n/latest/user/usage.html . 18 | 19 | """ 20 | 21 | import oslo_i18n as i18n 22 | 23 | 24 | _translators = i18n.TranslatorFactory(domain='keystonemiddleware') 25 | 26 | # The primary translation function using the well-known name "_" 27 | _ = _translators.primary 28 | -------------------------------------------------------------------------------- /keystonemiddleware/auth_token/_exceptions.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | from keystonemiddleware import exceptions 14 | 15 | 16 | ConfigurationError = exceptions.ConfigurationError 17 | 18 | 19 | class InvalidToken(exceptions.KeystoneMiddlewareException): 20 | pass 21 | 22 | 23 | class ServiceError(exceptions.KeystoneMiddlewareException): 24 | pass 25 | 26 | 27 | class RevocationListError(exceptions.KeystoneMiddlewareException): 28 | pass 29 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1649735-3c68f3243e474775.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - > 4 | [`bug 1649735 `_] 5 | The auth_token middleware no longer attempts to retrieve the revocation 6 | list from the Keystone server. The deprecated options 7 | `revocations_cache_time` and `check_revocations_for_cached` have been 8 | removed. 9 | 10 | Keystone no longer issues PKI/PKIZ tokens and now keystonemiddleware's 11 | Support for PKI/PKIZ and associated offline validation has been removed. 12 | This includes the deprecated config options `signing_dir`, and 13 | `hash_algorithms`. 14 | 15 | upgrade: 16 | - > 17 | [`bug 1649735 `_] 18 | Keystonemiddleware no longer supports PKI/PKIZ tokens, all 19 | associated offline validation has been removed. The configuration 20 | options `signing_dir`, and `hash_algorithms` have been removed, if 21 | they still exist in your configuration(s), they are now safe to remove. 22 | Please consider utilizing the newer fernet or JWS token formats. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT 17 | import setuptools 18 | 19 | # In python < 2.7.4, a lazy loading of package `pbr` will break 20 | # setuptools if some other modules registered functions in `atexit`. 21 | # solution from: http://bugs.python.org/issue15881#msg170215 22 | try: 23 | import multiprocessing # noqa 24 | except ImportError: 25 | pass 26 | 27 | setuptools.setup( 28 | setup_requires=['pbr>=2.0.0'], 29 | pbr=True) 30 | -------------------------------------------------------------------------------- /releasenotes/notes/deprecate-eventlet-unsafe-memcacheclientpool-f8b4a6733513d73e.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | deprecations: 3 | - | 4 | We no longer recommend using the eventlet unsafe keystonemiddleware's 5 | memcacheclientpool. This implementation may result in growing connections 6 | to memcached. 7 | 8 | It is recommended that the ``memcache_use_advanced_pool`` option 9 | is set to ``True`` in the ``keystone_authtoken`` configuration section of 10 | the various services (e.g. nova, glance, ...) when memcached is used for 11 | token cache. 12 | upgrade: 13 | - | 14 | [`bug 1892852 `_] 15 | [`bug 1888394 `_] 16 | [`bug 1883659 `_] 17 | Keystonemiddleware now using eventlet-safe implementation of 18 | ``MemcacheClientPool`` from oslo.cache's library by default. 19 | The ``keystonemiddleware`` implementation is now deprecated. For backwards 20 | compatibility, the ``[keystone_authtoken] memcache_use_advanced_pool`` 21 | option can be set to ``False`` config files of the various services (e.g. 22 | nova, glance, ...) when memcached is used for token cache. 23 | -------------------------------------------------------------------------------- /releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po: -------------------------------------------------------------------------------- 1 | # Gérald LONLAS , 2016. #zanata 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: keystonemiddleware Release Notes 4.14.1\n" 5 | "Report-Msgid-Bugs-To: \n" 6 | "POT-Creation-Date: 2017-04-05 12:59+0000\n" 7 | "MIME-Version: 1.0\n" 8 | "Content-Type: text/plain; charset=UTF-8\n" 9 | "Content-Transfer-Encoding: 8bit\n" 10 | "PO-Revision-Date: 2016-10-22 05:26+0000\n" 11 | "Last-Translator: Gérald LONLAS \n" 12 | "Language-Team: French\n" 13 | "Language: fr\n" 14 | "X-Generator: Zanata 3.9.6\n" 15 | "Plural-Forms: nplurals=2; plural=(n > 1)\n" 16 | 17 | msgid "4.1.0" 18 | msgstr "4.1.0" 19 | 20 | msgid "4.2.0" 21 | msgstr "4.2.0" 22 | 23 | msgid "4.3.0" 24 | msgstr "4.3.0" 25 | 26 | msgid "4.5.0" 27 | msgstr "4.5.0" 28 | 29 | msgid "4.6.0" 30 | msgstr "4.6.0" 31 | 32 | msgid "Bug Fixes" 33 | msgstr "Corrections de bugs" 34 | 35 | msgid "Current Series Release Notes" 36 | msgstr "Note de la release actuelle" 37 | 38 | msgid "Deprecation Notes" 39 | msgstr "Notes dépréciées " 40 | 41 | msgid "Mitaka Series Release Notes" 42 | msgstr "Note de release pour Mitaka" 43 | 44 | msgid "New Features" 45 | msgstr "Nouvelles fonctionnalités" 46 | 47 | msgid "Newton Series Release Notes" 48 | msgstr "Note de release pour Newton" 49 | 50 | msgid "Security Issues" 51 | msgstr "Problèmes de sécurités" 52 | 53 | msgid "keystonemiddleware Release Notes" 54 | msgstr "Note de release de keystonemiddleware" 55 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/test_entry_points.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import stevedore 14 | from testtools import matchers 15 | 16 | from keystonemiddleware.tests.unit import utils 17 | 18 | 19 | class TestPasteDeploymentEntryPoints(utils.BaseTestCase): 20 | 21 | def test_entry_points(self): 22 | expected_factory_names = [ 23 | 'audit', 24 | 'auth_token', 25 | 'ec2_token', 26 | 's3_token', 27 | ] 28 | em = stevedore.ExtensionManager('paste.filter_factory') 29 | 30 | exp_factories = set(['keystonemiddleware.' + name + ':filter_factory' 31 | for name in expected_factory_names]) 32 | actual_factories = set(['{0.__module__}:{0.__name__}'.format( 33 | extension.plugin) for extension in em]) 34 | # Ensure that all factories are defined by their names 35 | self.assertThat(actual_factories, matchers.ContainsAll(exp_factories)) 36 | -------------------------------------------------------------------------------- /releasenotes/source/locale/ko_KR/LC_MESSAGES/releasenotes.po: -------------------------------------------------------------------------------- 1 | # Ian Y. Choi , 2017. #zanata 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: keystonemiddleware Release Notes\n" 5 | "Report-Msgid-Bugs-To: \n" 6 | "POT-Creation-Date: 2018-02-20 19:27+0000\n" 7 | "MIME-Version: 1.0\n" 8 | "Content-Type: text/plain; charset=UTF-8\n" 9 | "Content-Transfer-Encoding: 8bit\n" 10 | "PO-Revision-Date: 2017-01-19 04:08+0000\n" 11 | "Last-Translator: Ian Y. Choi \n" 12 | "Language-Team: Korean (South Korea)\n" 13 | "Language: ko_KR\n" 14 | "X-Generator: Zanata 4.3.3\n" 15 | "Plural-Forms: nplurals=1; plural=0\n" 16 | 17 | msgid "4.1.0" 18 | msgstr "4.1.0" 19 | 20 | msgid "4.12.0" 21 | msgstr "4.12.0" 22 | 23 | msgid "4.2.0" 24 | msgstr "4.2.0" 25 | 26 | msgid "4.3.0" 27 | msgstr "4.3.0" 28 | 29 | msgid "4.5.0" 30 | msgstr "4.5.0" 31 | 32 | msgid "4.6.0" 33 | msgstr "4.6.0" 34 | 35 | msgid "Bug Fixes" 36 | msgstr "버그 수정" 37 | 38 | msgid "Current Series Release Notes" 39 | msgstr "최신 시리즈에 대한 릴리즈 노트" 40 | 41 | msgid "Deprecation Notes" 42 | msgstr "지원 종료된 기능 노트" 43 | 44 | msgid "Mitaka Series Release Notes" 45 | msgstr "Mitaka 시리즈에 대한 릴리즈 노트" 46 | 47 | msgid "New Features" 48 | msgstr "새로운 기능" 49 | 50 | msgid "Newton Series Release Notes" 51 | msgstr "Newton 시리즈에 대한 릴리즈 노트" 52 | 53 | msgid "Ocata Series Release Notes" 54 | msgstr "Ocata 시리즈에 대한 릴리즈 노트" 55 | 56 | msgid "Security Issues" 57 | msgstr "보안 이슈" 58 | 59 | msgid "Upgrade Notes" 60 | msgstr "업그레이드 노트" 61 | 62 | msgid "keystonemiddleware Release Notes" 63 | msgstr "keystonemiddleware 릴리즈 노트" 64 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | Team and repository tags 3 | ======================== 4 | 5 | .. image:: https://governance.openstack.org/tc/badges/keystonemiddleware.svg 6 | :target: https://governance.openstack.org/tc/reference/tags/index.html 7 | 8 | .. Change things from this point on 9 | 10 | Middleware for the OpenStack Identity API (Keystone) 11 | ==================================================== 12 | 13 | .. image:: https://img.shields.io/pypi/v/keystonemiddleware.svg 14 | :target: https://pypi.org/project/keystonemiddleware/ 15 | :alt: Latest Version 16 | 17 | .. image:: https://img.shields.io/pypi/dm/keystonemiddleware.svg 18 | :target: https://pypi.org/project/keystonemiddleware/ 19 | :alt: Downloads 20 | 21 | This package contains middleware modules designed to provide authentication and 22 | authorization features to web services other than `Keystone 23 | `. The most prominent module is 24 | ``keystonemiddleware.auth_token``. This package does not expose any CLI or 25 | Python API features. 26 | 27 | For information on contributing, see ``CONTRIBUTING.rst``. 28 | 29 | * License: Apache License, Version 2.0 30 | * Documentation: https://docs.openstack.org/keystonemiddleware/latest/ 31 | * Source: https://opendev.org/openstack/keystonemiddleware 32 | * Bugs: https://bugs.launchpad.net/keystonemiddleware 33 | * Release notes: https://docs.openstack.org/releasenotes/keystonemiddleware/ 34 | 35 | For any other information, refer to the parent project, Keystone: 36 | 37 | https://github.com/openstack/keystone 38 | -------------------------------------------------------------------------------- /doc/ext/apidoc.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 OpenStack Foundation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | # NOTE(blk-u): Uncomment the [pbr] section in setup.cfg and remove this 16 | # Sphinx extension when https://launchpad.net/bugs/1260495 is fixed. 17 | 18 | import os.path as path 19 | 20 | from sphinx import apidoc 21 | 22 | 23 | # NOTE(blk-u): pbr will run Sphinx multiple times when it generates 24 | # documentation. Once for each builder. To run this extension we use the 25 | # 'builder-inited' hook that fires at the beginning of a Sphinx build. 26 | # We use ``run_already`` to make sure apidocs are only generated once 27 | # even if Sphinx is run multiple times. 28 | run_already = False 29 | 30 | 31 | def run_apidoc(app): 32 | global run_already 33 | if run_already: 34 | return 35 | run_already = True 36 | 37 | package_dir = path.abspath(path.join(app.srcdir, '..', '..', 38 | 'keystonemiddleware')) 39 | source_dir = path.join(app.srcdir, 'api') 40 | apidoc.main(['apidoc', package_dir, '-f', 41 | '-H', 'keystonemiddleware Modules', 42 | '-o', source_dir]) 43 | 44 | 45 | def setup(app): 46 | app.connect('builder-inited', run_apidoc) 47 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = keystonemiddleware 3 | summary = Middleware for OpenStack Identity 4 | description_file = 5 | README.rst 6 | author = OpenStack 7 | author_email = openstack-discuss@lists.openstack.org 8 | home_page = https://docs.openstack.org/keystonemiddleware/latest/ 9 | python_requires = >=3.10 10 | classifier = 11 | Environment :: OpenStack 12 | Intended Audience :: Information Technology 13 | Intended Audience :: System Administrators 14 | License :: OSI Approved :: Apache Software License 15 | Operating System :: POSIX :: Linux 16 | Programming Language :: Python 17 | Programming Language :: Python :: 3 :: Only 18 | Programming Language :: Python :: 3 19 | Programming Language :: Python :: 3 :: Only 20 | Programming Language :: Python :: 3.10 21 | Programming Language :: Python :: 3.11 22 | Programming Language :: Python :: 3.12 23 | 24 | [files] 25 | packages = 26 | keystonemiddleware 27 | 28 | [extras] 29 | audit_notifications = 30 | oslo.messaging>=5.29.0 # Apache-2.0 31 | memcache_encryption = 32 | cryptography>=2.7 # BSD/Apache-2.0 33 | 34 | [entry_points] 35 | oslo.config.opts = 36 | keystonemiddleware.auth_token = keystonemiddleware.auth_token._opts:list_opts 37 | keystonemiddleware.audit = keystonemiddleware.audit:list_opts 38 | 39 | paste.filter_factory = 40 | auth_token = keystonemiddleware.auth_token:filter_factory 41 | audit = keystonemiddleware.audit:filter_factory 42 | ec2_token = keystonemiddleware.ec2_token:filter_factory 43 | s3_token = keystonemiddleware.s3_token:filter_factory 44 | oauth2_token = keystonemiddleware.oauth2_token:filter_factory 45 | oauth2_mtls_token = keystonemiddleware.oauth2_mtls_token:filter_factory 46 | external_oauth2_token = keystonemiddleware.external_oauth2_token:filter_factory 47 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/audit/test_logging_notifier.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | from unittest import mock 14 | 15 | import fixtures 16 | 17 | from keystonemiddleware.tests.unit.audit import base 18 | 19 | 20 | class TestLoggingNotifier(base.BaseAuditMiddlewareTest): 21 | 22 | def setUp(self): 23 | p = 'keystonemiddleware.audit._notifier.oslo_messaging' 24 | f = fixtures.MockPatch(p, None) 25 | self.messaging_fixture = self.useFixture(f) 26 | 27 | super(TestLoggingNotifier, self).setUp() 28 | 29 | def test_api_request_no_messaging(self): 30 | self.cfg.config(use_oslo_messaging=False, 31 | group='audit_middleware_notifications') 32 | app = self.create_simple_app() 33 | 34 | with mock.patch('keystonemiddleware.audit._LOG.info') as log: 35 | app.get('/foo/bar', extra_environ=self.get_environ_header()) 36 | 37 | # Check first notification with only 'request' 38 | call_args = log.call_args_list[0][0] 39 | self.assertEqual('audit.http.request', call_args[1]['event_type']) 40 | 41 | # Check second notification with request + response 42 | call_args = log.call_args_list[1][0] 43 | self.assertEqual('audit.http.response', call_args[1]['event_type']) 44 | -------------------------------------------------------------------------------- /releasenotes/notes/allow-expired-5ddbabcffc5678af.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | prelude: > 3 | Fetching expired tokens when using a valid service token is now allowed. 4 | This will help with long running operations that must continue between 5 | services longer than the original expiry of the token. 6 | features: 7 | - AuthToken middleware will now allow fetching an expired token when a valid 8 | service token is present. This service token must contain any one of the 9 | roles specified in ``service_token_roles``. 10 | - Service tokens are compared against a list of possible roles for validity. 11 | This will ensure that only services are submitting tokens as an 12 | ``X-Service-Token``. 13 | For backwards compatibility, if ``service_token_roles_required`` is not set, 14 | a warning will be emitted. To enforce the check properly, set 15 | ``service_token_roles_required`` to ``True``. It currently defaults to 16 | ``False`` 17 | upgrade: 18 | - Set the ``service_token_roles`` to a list of roles that services may have. 19 | The likely list is ``service`` or ``admin``. Any ``service_token_roles`` may 20 | apply to accept the service token. Ensure service users have one of these 21 | roles so interservice communication continues to work correctly. When verified, 22 | set the ``service_token_roles_required`` flag to ``True`` to enforce this 23 | behaviour. This will become the default setting in future releases. 24 | deprecations: 25 | - For backwards compatibility the ``service_token_roles_required`` option in 26 | ``[keystone_authtoken]`` was added. The option defaults to ``False`` and 27 | has been immediately deprecated. This will allow the current behaviour 28 | that service tokens are validated but not checked for roles to continue. 29 | The option should be set to ``True`` as soon as possible. The option will 30 | default to ``True`` in a future release. 31 | -------------------------------------------------------------------------------- /keystonemiddleware/echo/service.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | # implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | """ 15 | Run the echo service. 16 | 17 | The echo service can be run on port 8000 by executing the following:: 18 | 19 | $ python -m keystonemiddleware.echo 20 | 21 | When the ``auth_token`` module authenticates a request, the echo service 22 | will respond with all the environment variables presented to it by this 23 | module. 24 | """ 25 | 26 | from wsgiref import simple_server 27 | 28 | from oslo_serialization import jsonutils 29 | 30 | from keystonemiddleware import auth_token 31 | 32 | 33 | def echo_app(environ, start_response): 34 | """A WSGI application that echoes the CGI environment back to the user.""" 35 | start_response('200 OK', [('Content-Type', 'application/json')]) 36 | environment = dict((k, v) for k, v in environ.items() 37 | if k.startswith('HTTP_X_')) 38 | yield jsonutils.dumps(environment) 39 | 40 | 41 | class EchoService(object): 42 | """Runs an instance of the echo app on init.""" 43 | 44 | def __init__(self): 45 | # hardcode any non-default configuration here 46 | conf = {'auth_protocol': 'http', 'admin_token': 'ADMIN'} 47 | app = auth_token.AuthProtocol(echo_app, conf) 48 | server = simple_server.make_server('', 8000, app) 49 | print('Serving on port 8000 (Ctrl+C to end)...') 50 | server.serve_forever() 51 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | ======================================================= 2 | Python Middleware for OpenStack Identity API (Keystone) 3 | ======================================================= 4 | 5 | This is the middleware provided for integrating with the OpenStack 6 | Identity API and handling authorization enforcement based upon the 7 | data within the OpenStack Identity tokens. Also included is middleware that 8 | provides the ability to create audit events based on API requests. 9 | 10 | Contents: 11 | 12 | .. toctree:: 13 | :maxdepth: 1 14 | 15 | middlewarearchitecture 16 | audit 17 | installation 18 | 19 | Related Identity Projects 20 | ========================= 21 | 22 | In addition to creating the Python Middleware for OpenStack Identity 23 | API, the Keystone team also provides `Identity Service`_, as well as 24 | `Python Client Library`_. 25 | 26 | .. _`Identity Service`: https://docs.openstack.org/keystone/latest/ 27 | .. _`Python Client Library`: https://docs.openstack.org/python-keystoneclient/latest/ 28 | 29 | Release Notes 30 | ============= 31 | 32 | `Release Notes`_ 33 | 34 | .. _Release Notes: https://docs.openstack.org/releasenotes/keystonemiddleware/ 35 | 36 | Contributing 37 | ============ 38 | 39 | Code is hosted `on GitHub`_. Submit bugs to the Keystone project on 40 | `Launchpad`_. Submit code to the ``openstack/keystonemiddleware`` project 41 | using `Gerrit`_. 42 | 43 | .. _on GitHub: https://github.com/openstack/keystonemiddleware 44 | .. _Launchpad: https://launchpad.net/keystonemiddleware 45 | .. _Gerrit: https://docs.openstack.org/infra/manual/developers.html#development-workflow 46 | 47 | Run tests with ``tox -e py27`` if running with python 2.7. See the 48 | ``tox.ini`` file for other test environment options. 49 | 50 | Code Documentation 51 | ================== 52 | .. toctree:: 53 | :maxdepth: 1 54 | 55 | api/modules 56 | 57 | Indices and tables 58 | ================== 59 | 60 | * :ref:`genindex` 61 | * :ref:`modindex` 62 | * :ref:`search` 63 | 64 | -------------------------------------------------------------------------------- /keystonemiddleware/audit/_notifier.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import os 14 | import sys 15 | 16 | try: 17 | import oslo_messaging 18 | except ImportError: 19 | oslo_messaging = None 20 | 21 | 22 | class _LogNotifier(object): 23 | 24 | def __init__(self, log): 25 | self._log = log 26 | 27 | def notify(self, context, event_type, payload): 28 | self._log.info('Event type: %(event_type)s, Context: %(context)s, ' 29 | 'Payload: %(payload)s', {'context': context, 30 | 'event_type': event_type, 31 | 'payload': payload}) 32 | 33 | 34 | class _MessagingNotifier(object): 35 | 36 | def __init__(self, notifier): 37 | self._notifier = notifier 38 | 39 | def notify(self, context, event_type, payload): 40 | self._notifier.info(context, event_type, payload) 41 | 42 | 43 | def create_notifier(conf, log): 44 | if oslo_messaging and conf.get('use_oslo_messaging'): 45 | transport = oslo_messaging.get_notification_transport( 46 | conf.oslo_conf_obj, 47 | url=conf.get('transport_url')) 48 | 49 | notifier = oslo_messaging.Notifier( 50 | transport, 51 | os.path.basename(sys.argv[0]), 52 | driver=conf.get('driver'), 53 | topics=conf.get('topics')) 54 | 55 | return _MessagingNotifier(notifier) 56 | 57 | else: 58 | return _LogNotifier(log) 59 | -------------------------------------------------------------------------------- /keystonemiddleware/locale/ko_KR/LC_MESSAGES/keystonemiddleware.po: -------------------------------------------------------------------------------- 1 | # Ian Y. Choi , 2017. #zanata 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: keystonemiddleware VERSION\n" 5 | "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" 6 | "POT-Creation-Date: 2019-12-21 02:49+0000\n" 7 | "MIME-Version: 1.0\n" 8 | "Content-Type: text/plain; charset=UTF-8\n" 9 | "Content-Transfer-Encoding: 8bit\n" 10 | "PO-Revision-Date: 2017-01-23 03:52+0000\n" 11 | "Last-Translator: Ian Y. Choi \n" 12 | "Language-Team: Korean (South Korea)\n" 13 | "Language: ko_KR\n" 14 | "X-Generator: Zanata 4.3.3\n" 15 | "Plural-Forms: nplurals=1; plural=0\n" 16 | 17 | msgid "Access key not provided" 18 | msgstr "Access key가 없습니다" 19 | 20 | msgid "Encrypted data appears to be corrupted." 21 | msgstr "암호화된 데이터가 손상된 것으로 보입니다." 22 | 23 | #, python-format 24 | msgid "Error response from keystone: %s" 25 | msgstr "keystone 오류 응답: %s" 26 | 27 | msgid "Failed to fetch token data from identity server" 28 | msgstr "인증 서버로부터 토큰 데이터를 가져올 수 없습니다" 29 | 30 | msgid "Failure parsing response from keystone" 31 | msgstr "keystone 응답 파싱 실패" 32 | 33 | msgid "Identity server rejected authorization necessary to fetch token data" 34 | msgstr "토큰 데이터를 가져오기 위해 필수인 인증을 인증 서버에서 거절" 35 | 36 | msgid "Invalid MAC; data appears to be corrupted." 37 | msgstr "잘못된 MAC; 데이터가 손상된 것으로 보입니다." 38 | 39 | msgid "Invalid version asked for in auth_token plugin" 40 | msgstr "auth_token 플러그인에서 잘못된 버전 요청" 41 | 42 | msgid "No compatible apis supported by server" 43 | msgstr "서버에서 지원하는 호환 api가 없습니다" 44 | 45 | msgid "Signature not provided" 46 | msgstr "Signature가 없습니다" 47 | 48 | msgid "The request you have made requires authentication." 49 | msgstr "제공한 요청은 인증을 필요로 합니다." 50 | 51 | msgid "Token authorization failed" 52 | msgstr "Token 인증 실패" 53 | 54 | msgid "Unable to determine service tenancy." 55 | msgstr "서비스 tenancy를 결정할 수 없습니다." 56 | 57 | msgid "" 58 | "memcache_secret_key must be defined when a memcache_security_strategy is " 59 | "defined" 60 | msgstr "" 61 | "memcache_security_strategy 가 정의되어 있을 때 memcache_secret_key 를 정의해" 62 | "야 합니다" 63 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/auth_token/base.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import http.client as http_client 14 | 15 | import fixtures 16 | from oslo_config import cfg 17 | from oslo_config import fixture as cfg_fixture 18 | from oslo_log import log as logging 19 | from requests_mock.contrib import fixture as rm_fixture 20 | import webob.dec 21 | 22 | from keystonemiddleware import auth_token 23 | from keystonemiddleware.tests.unit import utils 24 | 25 | 26 | class BaseAuthTokenTestCase(utils.MiddlewareTestCase): 27 | 28 | def setUp(self): 29 | super(BaseAuthTokenTestCase, self).setUp() 30 | self.requests_mock = self.useFixture(rm_fixture.Fixture()) 31 | self.logger = fixtures.FakeLogger(level=logging.DEBUG) 32 | self.cfg = self.useFixture(cfg_fixture.Config(conf=cfg.ConfigOpts())) 33 | self.cfg.conf(args=[]) 34 | 35 | def create_middleware(self, cb, conf=None, use_global_conf=False): 36 | 37 | @webob.dec.wsgify 38 | def _do_cb(req): 39 | return cb(req) 40 | 41 | if use_global_conf: 42 | opts = conf or {} 43 | else: 44 | opts = { 45 | 'oslo_config_config': self.cfg.conf, 46 | } 47 | opts.update(conf or {}) 48 | 49 | return auth_token.AuthProtocol(_do_cb, opts) 50 | 51 | def call(self, middleware, method='GET', path='/', headers=None, 52 | expected_status=http_client.OK, 53 | expected_body_string=None): 54 | req = webob.Request.blank(path) 55 | req.method = method 56 | 57 | for k, v in (headers or {}).items(): 58 | req.headers[k] = v 59 | 60 | resp = req.get_response(middleware) 61 | self.assertEqual(expected_status, resp.status_int) 62 | if expected_body_string: 63 | self.assertIn(expected_body_string, str(resp.body)) 64 | resp.request = req 65 | return resp 66 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 4.2.0 3 | envlist = py3,pep8,releasenotes 4 | 5 | [testenv] 6 | usedevelop = True 7 | setenv = 8 | VIRTUAL_ENV={envdir} 9 | OS_STDOUT_NOCAPTURE=False 10 | OS_STDERR_NOCAPTURE=False 11 | deps = 12 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 13 | -r{toxinidir}/requirements.txt 14 | -r{toxinidir}/test-requirements.txt 15 | commands = 16 | stestr run {posargs} 17 | 18 | [testenv:pep8] 19 | commands = 20 | flake8 21 | bandit -r keystonemiddleware -x tests -n5 22 | 23 | [testenv:venv] 24 | commands = {posargs} 25 | 26 | [testenv:cover] 27 | setenv = 28 | {[testenv]setenv} 29 | PYTHON=coverage run --source keystonemiddleware --parallel-mode 30 | commands = 31 | stestr run {posargs} 32 | coverage combine 33 | coverage html -d cover 34 | coverage xml -o cover/coverage.xml 35 | 36 | [testenv:debug] 37 | commands = oslo_debug_helper -t keystonemiddleware/tests {posargs} 38 | 39 | [testenv:docs] 40 | deps = 41 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 42 | -r{toxinidir}/doc/requirements.txt 43 | commands= 44 | doc8 doc/source 45 | sphinx-build -W -b html doc/source doc/build/html 46 | 47 | [testenv:pdf-docs] 48 | deps = {[testenv:docs]deps} 49 | allowlist_externals = 50 | make 51 | rm 52 | commands = 53 | rm -rf doc/build/pdf 54 | sphinx-build -W -b latex doc/source doc/build/pdf 55 | make -C doc/build/pdf 56 | 57 | [testenv:releasenotes] 58 | deps = 59 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 60 | -r{toxinidir}/doc/requirements.txt 61 | commands = 62 | sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html 63 | 64 | [flake8] 65 | # D100: Missing docstring in public module 66 | # D101: Missing docstring in public class 67 | # D102: Missing docstring in public method 68 | # D103: Missing docstring in public function 69 | # D104: Missing docstring in public package 70 | # D107: Missing docstring in __init__ 71 | # D401: First line should be in imperative mood 72 | # W503 line break before binary operator 73 | # W504 line break after binary operator 74 | ignore = D100,D101,D102,D103,D104,D107,D401,W503,W504 75 | show-source = True 76 | exclude = .venv,.tox,dist,doc,*egg,build 77 | 78 | [hacking] 79 | import_exceptions = 80 | keystonemiddleware.i18n 81 | 82 | [doc8] 83 | extensions = .rst, .yaml 84 | # lines should not be longer than 79 characters. 85 | max-line-length = 79 86 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/test_access_rules.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 SUSE LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from keystonemiddleware.auth_token import _path_matches 16 | from keystonemiddleware.tests.unit import utils 17 | 18 | 19 | class TestAccessRules(utils.BaseTestCase): 20 | 21 | def test_path_matches(self): 22 | good_matches = [ 23 | ('/v2/servers', '/v2/servers'), 24 | ('/v2/servers/123', '/v2/servers/{server_id}'), 25 | ('/v2/servers/123/', '/v2/servers/{server_id}/'), 26 | ('/v2/servers/123', '/v2/servers/*'), 27 | ('/v2/servers/123/', '/v2/servers/*/'), 28 | ('/v2/servers/123', '/v2/servers/**'), 29 | ('/v2/servers/123/', '/v2/servers/**'), 30 | ('/v2/servers/123/456', '/v2/servers/**'), 31 | ('/v2/servers', '**'), 32 | ('/v2/servers/', '**'), 33 | ('/v2/servers/123', '**'), 34 | ('/v2/servers/123/456', '**'), 35 | ('/v2/servers/123/volume/456', '**'), 36 | ('/v2/servers/123/456', '/v2/*/*/*'), 37 | ('/v2/123/servers/466', '/v2/{project_id}/servers/{server_id}'), 38 | ] 39 | for (request, pattern) in good_matches: 40 | self.assertIsNotNone(_path_matches(request, pattern)) 41 | bad_matches = [ 42 | ('/v2/servers/someuuid', '/v2/servers'), 43 | ('/v2/servers//', '/v2/servers/{server_id}'), 44 | ('/v2/servers/123/', '/v2/servers/{server_id}'), 45 | ('/v2/servers/123/456', '/v2/servers/{server_id}'), 46 | ('/v2/servers/123/456', '/v2/servers/*'), 47 | ('/v2/servers', 'v2/servers'), 48 | ('/v2/servers/123/456/789', '/v2/*/*/*'), 49 | ('/v2/servers/123/', '/v2/*/*/*'), 50 | ('/v2/servers/', '/v2/servers/{server_id}'), 51 | ('/v2/servers', '/v2/servers/{server_id}'), 52 | ] 53 | for (request, pattern) in bad_matches: 54 | self.assertIsNone(_path_matches(request, pattern)) 55 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/test_fixtures.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import datetime 14 | import uuid 15 | 16 | from oslo_utils import timeutils 17 | 18 | from keystonemiddleware import fixture 19 | from keystonemiddleware.tests.unit.auth_token import test_auth_token_middleware 20 | 21 | 22 | class AuthTokenFixtureTest( 23 | test_auth_token_middleware.BaseAuthTokenMiddlewareTest): 24 | 25 | def setUp(self): 26 | self.token_id = uuid.uuid4().hex 27 | self.user_id = uuid.uuid4().hex 28 | self.username = uuid.uuid4().hex 29 | self.project_id = uuid.uuid4().hex 30 | self.project_name = uuid.uuid4().hex 31 | self.role_list = [uuid.uuid4().hex, uuid.uuid4().hex] 32 | super(AuthTokenFixtureTest, self).setUp() 33 | 34 | self.atm_fixture = self.useFixture(fixture.AuthTokenFixture()) 35 | self.atm_fixture.add_token_data(token_id=self.token_id, 36 | user_id=self.user_id, 37 | user_name=self.username, 38 | role_list=self.role_list, 39 | project_id=self.project_id, 40 | project_name=self.project_name) 41 | self.set_middleware() 42 | self.middleware._app.expected_env = { 43 | 'HTTP_X_USER_ID': self.user_id, 44 | 'HTTP_X_USER_NAME': self.username, 45 | 'HTTP_X_PROJECT_ID': self.project_id, 46 | 'HTTP_X_PROJECT_NAME': self.project_name, 47 | 'HTTP_X_ROLES': ','.join(self.role_list)} 48 | 49 | def test_auth_token_fixture_valid_token(self): 50 | resp = self.call_middleware(headers={'X-Auth-Token': self.token_id}) 51 | self.assertIn('keystone.token_info', resp.request.environ) 52 | 53 | def test_auth_token_fixture_invalid_token(self): 54 | self.call_middleware( 55 | headers={'X-Auth-Token': uuid.uuid4().hex}, expected_status=401) 56 | 57 | def test_auth_token_fixture_expired_token(self): 58 | expired_token_id = uuid.uuid4().hex 59 | self.atm_fixture.add_token_data( 60 | token_id=expired_token_id, 61 | user_id=self.user_id, 62 | role_list=self.role_list, 63 | expires=(timeutils.utcnow() - datetime.timedelta(seconds=86400))) 64 | self.call_middleware( 65 | headers={'X-Auth-Token': expired_token_id}, expected_status=401) 66 | -------------------------------------------------------------------------------- /doc/source/images/graphs_authComp.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | AuthComp 11 | 12 | 13 | AuthComp 14 | 15 | Auth 16 | Component 17 | 18 | 19 | 20 | AuthComp->Reject 21 | 22 | 23 | Reject 24 | Unauthenticated 25 | Requests 26 | 27 | 28 | Service 29 | 30 | OpenStack 31 | Service 32 | 33 | 34 | AuthComp->Service 35 | 36 | 37 | Forward 38 | Authenticated 39 | Requests 40 | 41 | 42 | 43 | Start->AuthComp 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /keystonemiddleware/auth_token/_user_plugin.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | from keystoneauth1.identity import base as base_identity 14 | 15 | 16 | def _log_format(auth_ref): 17 | roles = ','.join(auth_ref.role_names) 18 | return 'user_id %s, project_id %s, roles %s' % (auth_ref.user_id, 19 | auth_ref.project_id, 20 | roles) 21 | 22 | 23 | class UserAuthPlugin(base_identity.BaseIdentityPlugin): 24 | """The incoming authentication credentials. 25 | 26 | A plugin that represents the incoming user credentials. This can be 27 | consumed by applications. 28 | 29 | This object is not expected to be constructed directly by users. It is 30 | created and passed by auth_token middleware and then can be used as the 31 | authentication plugin when communicating via a session. 32 | """ 33 | 34 | def __init__(self, user_auth_ref, serv_auth_ref, session=None, auth=None): 35 | super(UserAuthPlugin, self).__init__(reauthenticate=False) 36 | 37 | self.user = user_auth_ref 38 | self.service = serv_auth_ref 39 | 40 | # NOTE(jamielennox): adding a service token requires the original 41 | # session and auth plugin from auth_token 42 | self._session = session 43 | self._auth = auth 44 | 45 | @property 46 | def has_user_token(self): 47 | """Did this authentication request contained a user auth token.""" 48 | return self.user is not None 49 | 50 | @property 51 | def has_service_token(self): 52 | """Did this authentication request contained a service token.""" 53 | return self.service is not None 54 | 55 | def get_auth_ref(self, session, **kwargs): 56 | # NOTE(jamielennox): We will always use the auth_ref that was 57 | # calculated by the middleware. reauthenticate=False in __init__ should 58 | # ensure that this function is only called on the first access. 59 | return self.user 60 | 61 | @property 62 | def _log_format(self): 63 | msg = [] 64 | 65 | if self.has_user_token: 66 | msg.append('user: %s' % _log_format(self.user)) 67 | 68 | if self.has_service_token: 69 | msg.append('service: %s' % _log_format(self.service)) 70 | 71 | return ' '.join(msg) 72 | 73 | def get_headers(self, session, **kwargs): 74 | headers = super(UserAuthPlugin, self).get_headers(session, **kwargs) 75 | 76 | if headers is not None and self._session: 77 | token = self._session.get_token(auth=self._auth) 78 | 79 | if token: 80 | headers['X-Service-Token'] = token 81 | 82 | return headers 83 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/audit/base.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | 14 | from oslo_config import fixture as cfg_fixture 15 | from oslo_messaging import conffixture as msg_fixture 16 | from oslotest import createfile 17 | import webob.dec 18 | 19 | from keystonemiddleware import audit 20 | from keystonemiddleware.tests.unit import utils 21 | 22 | 23 | audit_map_content = """ 24 | [custom_actions] 25 | reboot = start/reboot 26 | os-migrations/get = read 27 | 28 | [path_keywords] 29 | action = None 30 | os-hosts = host 31 | os-migrations = None 32 | reboot = None 33 | servers = server 34 | 35 | [service_endpoints] 36 | compute = service/compute 37 | """ 38 | 39 | 40 | class BaseAuditMiddlewareTest(utils.MiddlewareTestCase): 41 | PROJECT_NAME = 'keystonemiddleware' 42 | 43 | def setUp(self): 44 | super(BaseAuditMiddlewareTest, self).setUp() 45 | 46 | self.audit_map_file_fixture = self.useFixture( 47 | createfile.CreateFileWithContent('audit', audit_map_content)) 48 | 49 | self.cfg = self.useFixture(cfg_fixture.Config()) 50 | self.msg = self.useFixture(msg_fixture.ConfFixture(self.cfg.conf)) 51 | 52 | self.cfg.conf([], project=self.PROJECT_NAME) 53 | 54 | def create_middleware(self, cb, **kwargs): 55 | 56 | @webob.dec.wsgify 57 | def _do_cb(req): 58 | return cb(req) 59 | 60 | kwargs.setdefault('audit_map_file', self.audit_map) 61 | kwargs.setdefault('service_name', 'pycadf') 62 | 63 | return audit.AuditMiddleware(_do_cb, **kwargs) 64 | 65 | @property 66 | def audit_map(self): 67 | return self.audit_map_file_fixture.path 68 | 69 | @staticmethod 70 | def get_environ_header(req_type=None): 71 | env_headers = {'HTTP_X_SERVICE_CATALOG': 72 | '''[{"endpoints_links": [], 73 | "endpoints": [{"adminURL": 74 | "http://admin_host:8774", 75 | "region": "RegionOne", 76 | "publicURL": 77 | "http://public_host:8774", 78 | "internalURL": 79 | "http://internal_host:8774", 80 | "id": "resource_id"}], 81 | "type": "compute", 82 | "name": "nova"}]''', 83 | 'HTTP_X_USER_ID': 'user_id', 84 | 'HTTP_X_USER_NAME': 'user_name', 85 | 'HTTP_X_AUTH_TOKEN': 'token', 86 | 'HTTP_X_PROJECT_ID': 'tenant_id', 87 | 'HTTP_X_IDENTITY_STATUS': 'Confirmed'} 88 | if req_type: 89 | env_headers['REQUEST_METHOD'] = req_type 90 | return env_headers 91 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXSOURCE = source 8 | PAPER = 9 | BUILDDIR = build 10 | 11 | # Internal variables. 12 | PAPEROPT_a4 = -D latex_paper_size=a4 13 | PAPEROPT_letter = -D latex_paper_size=letter 14 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SPHINXSOURCE) 15 | 16 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 17 | 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 27 | @echo " changes to make an overview of all changed/added/deprecated items" 28 | @echo " linkcheck to check all external links for integrity" 29 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 30 | 31 | clean: 32 | -rm -rf $(BUILDDIR)/* 33 | 34 | html: 35 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 36 | @echo 37 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 38 | 39 | dirhtml: 40 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 43 | 44 | pickle: 45 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 46 | @echo 47 | @echo "Build finished; now you can process the pickle files." 48 | 49 | json: 50 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 51 | @echo 52 | @echo "Build finished; now you can process the JSON files." 53 | 54 | htmlhelp: 55 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 56 | @echo 57 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 58 | ".hhp project file in $(BUILDDIR)/htmlhelp." 59 | 60 | qthelp: 61 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 62 | @echo 63 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 64 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 65 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/keystonemiddleware.qhcp" 66 | @echo "To view the help file:" 67 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/keystonemiddleware.qhc" 68 | 69 | latex: 70 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 71 | @echo 72 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 73 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 74 | "run these through (pdf)latex." 75 | 76 | changes: 77 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 78 | @echo 79 | @echo "The overview file is in $(BUILDDIR)/changes." 80 | 81 | linkcheck: 82 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 83 | @echo 84 | @echo "Link check complete; look for any errors in the above output " \ 85 | "or in $(BUILDDIR)/linkcheck/output.txt." 86 | 87 | doctest: 88 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 89 | @echo "Testing of doctests in the sources finished, look at the " \ 90 | "results in $(BUILDDIR)/doctest/output.txt." 91 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/auth_token/test_memcache_crypt.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import struct 14 | 15 | from keystonemiddleware.auth_token import _memcache_crypt as memcache_crypt 16 | from keystonemiddleware.tests.unit import utils 17 | 18 | 19 | class MemcacheCryptPositiveTests(utils.BaseTestCase): 20 | def _setup_keys(self, strategy): 21 | return memcache_crypt.derive_keys('token', 'secret', strategy) 22 | 23 | def test_derive_keys(self): 24 | keys = self._setup_keys(b'strategy') 25 | self.assertEqual(len(keys['ENCRYPTION']), 26 | len(keys['CACHE_KEY'])) 27 | self.assertEqual(len(keys['CACHE_KEY']), 28 | len(keys['MAC'])) 29 | self.assertNotEqual(keys['ENCRYPTION'], 30 | keys['MAC']) 31 | self.assertIn('strategy', keys.keys()) 32 | 33 | def test_key_strategy_diff(self): 34 | k1 = self._setup_keys(b'MAC') 35 | k2 = self._setup_keys(b'ENCRYPT') 36 | self.assertNotEqual(k1, k2) 37 | 38 | def test_sign_data(self): 39 | keys = self._setup_keys(b'MAC') 40 | sig = memcache_crypt.sign_data(keys['MAC'], b'data') 41 | self.assertEqual(len(sig), memcache_crypt.DIGEST_LENGTH_B64) 42 | 43 | def test_encryption(self): 44 | int2byte = struct.Struct(">B").pack 45 | keys = self._setup_keys(b'ENCRYPT') 46 | # what you put in is what you get out 47 | for data in [b'data', b'1234567890123456', b'\x00\xFF' * 13 48 | ] + [int2byte(x % 256) * x for x in range(768)]: 49 | crypt = memcache_crypt.encrypt_data(keys['ENCRYPTION'], data) 50 | decrypt = memcache_crypt.decrypt_data(keys['ENCRYPTION'], crypt) 51 | self.assertEqual(data, decrypt) 52 | self.assertRaises(memcache_crypt.DecryptError, 53 | memcache_crypt.decrypt_data, 54 | keys['ENCRYPTION'], crypt[:-1]) 55 | 56 | def test_protect_wrappers(self): 57 | data = b'My Pretty Little Data' 58 | for strategy in [b'MAC', b'ENCRYPT']: 59 | keys = self._setup_keys(strategy) 60 | protected = memcache_crypt.protect_data(keys, data) 61 | self.assertNotEqual(protected, data) 62 | if strategy == b'ENCRYPT': 63 | self.assertNotIn(data, protected) 64 | unprotected = memcache_crypt.unprotect_data(keys, protected) 65 | self.assertEqual(data, unprotected) 66 | self.assertRaises(memcache_crypt.InvalidMacError, 67 | memcache_crypt.unprotect_data, 68 | keys, protected[:-1]) 69 | self.assertIsNone(memcache_crypt.unprotect_data(keys, None)) 70 | 71 | def test_no_cryptography(self): 72 | aes = memcache_crypt.ciphers 73 | memcache_crypt.ciphers = None 74 | self.assertRaises(memcache_crypt.CryptoUnavailableError, 75 | memcache_crypt.encrypt_data, 'token', 'secret', 76 | 'data') 77 | memcache_crypt.ciphers = aes 78 | -------------------------------------------------------------------------------- /doc/source/images/graphs_authCompDelegate.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | AuthCompDelegate 11 | 12 | 13 | AuthComp 14 | 15 | Auth 16 | Component 17 | 18 | 19 | 20 | AuthComp->Reject 21 | 22 | 23 | Reject Requests 24 | Indicated by the Service 25 | 26 | 27 | Service 28 | 29 | OpenStack 30 | Service 31 | 32 | 33 | AuthComp->Service 34 | 35 | 36 | Forward Requests 37 | with Identiy Status 38 | 39 | 40 | Service->AuthComp 41 | 42 | 43 | Send Response OR 44 | Reject Message 45 | 46 | 47 | 48 | Start->AuthComp 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /keystonemiddleware/fixture.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import uuid 14 | 15 | import fixtures 16 | from keystoneauth1 import fixture as client_fixtures 17 | from oslo_log import log as logging 18 | from oslo_utils import timeutils 19 | 20 | from keystonemiddleware import auth_token 21 | from keystonemiddleware.auth_token import _exceptions 22 | 23 | 24 | _LOG = logging.getLogger(__name__) 25 | 26 | 27 | class AuthTokenFixture(fixtures.Fixture): 28 | """Overrides what keystonemiddleware will return to the app behind it.""" 29 | 30 | def setUp(self): 31 | super(AuthTokenFixture, self).setUp() 32 | # Ensure that the initialized token data is cleaned up 33 | self._token_data = {} 34 | self.addCleanup(self._token_data.clear) 35 | _LOG.info('Using Testing AuthTokenFixture...') 36 | self.useFixture(fixtures.MockPatchObject( 37 | auth_token.AuthProtocol, 38 | 'fetch_token', 39 | self.fetch_token)) 40 | 41 | @property 42 | def tokens(self): 43 | return self._token_data.keys() 44 | 45 | def add_token_data(self, token_id=None, expires=None, 46 | user_id=None, user_name=None, 47 | user_domain_id=None, user_domain_name=None, 48 | project_id=None, project_name=None, 49 | project_domain_id=None, project_domain_name=None, 50 | role_list=None, is_v2=False): 51 | """Add token data to the auth_token fixture.""" 52 | if not token_id: 53 | token_id = uuid.uuid4().hex 54 | 55 | if not role_list: 56 | role_list = [] 57 | 58 | if is_v2: 59 | token = client_fixtures.V2Token( 60 | token_id=token_id, expires=expires, tenant_id=project_id, 61 | tenant_name=project_name, user_id=user_id, user_name=user_name) 62 | else: 63 | token = client_fixtures.V3Token( 64 | expires=expires, user_id=user_id, user_name=user_name, 65 | user_domain_id=user_domain_id, project_id=project_id, 66 | project_name=project_name, 67 | project_domain_id=project_domain_id, 68 | user_domain_name=user_domain_name, 69 | project_domain_name=project_domain_name) 70 | for role in role_list: 71 | token.add_role(name=role) 72 | self.add_token(token, token_id=token_id) 73 | 74 | def add_token(self, token_data, token_id=None): 75 | """Add an existing token to the middleware. 76 | 77 | :param token_data: token data to add to the fixture 78 | :type token_data: dict 79 | :param token_id: the token ID to add this token as 80 | :type token_id: str 81 | :returns: The token_id that the token was added as. 82 | :rtype: str 83 | """ 84 | if not token_id: 85 | token_id = uuid.uuid4().hex 86 | self._token_data[token_id] = token_data 87 | return token_id 88 | 89 | def fetch_token(self, token, **kwargs): 90 | """Low level replacement of fetch_token for AuthProtocol.""" 91 | token_data = self._token_data.get(token, {}) 92 | if token_data: 93 | self._assert_token_not_expired(token_data.expires) 94 | return token_data 95 | raise _exceptions.InvalidToken() 96 | 97 | def _assert_token_not_expired(self, token_expires): 98 | if timeutils.utcnow() > timeutils.normalize_time(token_expires): 99 | raise _exceptions.InvalidToken() 100 | -------------------------------------------------------------------------------- /keystonemiddleware/oauth2_token.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 OpenStack Foundation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import webob 16 | 17 | from oslo_log import log as logging 18 | from oslo_serialization import jsonutils 19 | 20 | from keystonemiddleware.auth_token import _user_plugin 21 | from keystonemiddleware.auth_token import AuthProtocol 22 | from keystonemiddleware import exceptions 23 | from keystonemiddleware.i18n import _ 24 | 25 | 26 | _LOG = logging.getLogger(__name__) 27 | 28 | 29 | class OAuth2Protocol(AuthProtocol): 30 | """Middleware that handles OAuth2.0 client credentials authentication.""" 31 | 32 | def __init__(self, app, conf): 33 | log = logging.getLogger(conf.get('log_name', __name__)) 34 | log.info('Starting Keystone oauth2_token middleware') 35 | super(OAuth2Protocol, self).__init__(app, conf) 36 | 37 | def _is_valid_access_token(self, request): 38 | """Check if the request contains an OAuth2.0 access token. 39 | 40 | :param request: Incoming request 41 | :type request: _request.AuthTokenRequest 42 | """ 43 | access_token = None 44 | if (request.authorization and 45 | request.authorization.authtype == 'Bearer'): 46 | access_token = request.authorization.params 47 | 48 | if access_token: 49 | try: 50 | token_data, user_auth_ref = self._do_fetch_token( 51 | access_token, allow_expired=False) 52 | self._validate_token(user_auth_ref, 53 | allow_expired=False) 54 | token = token_data['token'] 55 | self.validate_allowed_request(request, token) 56 | self._confirm_token_bind(user_auth_ref, request) 57 | request.token_info = token_data 58 | request.token_auth = _user_plugin.UserAuthPlugin( 59 | user_auth_ref, None) 60 | return True 61 | except exceptions.KeystoneMiddlewareException as err: 62 | _LOG.info('Invalid OAuth2.0 access token: %s' % str(err)) 63 | return False 64 | 65 | def process_request(self, request): 66 | """Process request. 67 | 68 | :param request: Incoming request 69 | :type request: _request.AuthTokenRequest 70 | """ 71 | request.remove_auth_headers() 72 | self._token_cache.initialize(request.environ) 73 | if (not self._is_valid_access_token(request) 74 | or "keystone.token_info" not in request.environ 75 | or "token" not in request.environ["keystone.token_info"]): 76 | _LOG.info('Rejecting request') 77 | message = _('The request you have made requires authentication.') 78 | body = {'error': { 79 | 'code': 401, 80 | 'title': 'Unauthorized', 81 | 'message': message, 82 | }} 83 | raise webob.exc.HTTPUnauthorized( 84 | body=jsonutils.dumps(body), 85 | headers=self._reject_auth_headers, 86 | charset='UTF-8', 87 | content_type='application/json') 88 | 89 | request.set_user_headers(request.token_auth.user) 90 | request.set_service_catalog_headers(request.token_auth.user) 91 | request.token_auth._auth = self._auth 92 | request.token_auth._session = self._session 93 | 94 | 95 | def filter_factory(global_conf, **local_conf): 96 | """Return a WSGI filter app for use with paste.deploy.""" 97 | conf = global_conf.copy() 98 | conf.update(local_conf) 99 | 100 | def auth_filter(app): 101 | return OAuth2Protocol(app, conf) 102 | 103 | return auth_filter 104 | -------------------------------------------------------------------------------- /doc/source/audit.rst: -------------------------------------------------------------------------------- 1 | .. 2 | Copyright 2014 IBM Corp 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | not use this file except in compliance with the License. You may obtain 6 | a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | License for the specific language governing permissions and limitations 14 | under the License. 15 | 16 | .. _middleware: 17 | 18 | ================ 19 | Audit middleware 20 | ================ 21 | 22 | The Keystone middleware library provides an optional WSGI middleware filter 23 | which allows the ability to audit API requests for each component of OpenStack. 24 | 25 | The audit middleware filter utilises environment variables to build the CADF 26 | event. 27 | 28 | .. figure:: ./images/audit.png 29 | :width: 100% 30 | :align: center 31 | :alt: Figure 1: Audit middleware in Nova pipeline 32 | 33 | The figure above shows the middleware in Nova's pipeline. 34 | 35 | Enabling audit middleware 36 | ========================= 37 | To enable auditing, oslo.messaging_ should be installed. If not, the middleware 38 | will log the audit event instead. Auditing can be enabled for a specific 39 | project by editing the project's api-paste.ini file to include the following 40 | filter definition: 41 | 42 | :: 43 | 44 | [filter:audit] 45 | paste.filter_factory = keystonemiddleware.audit:filter_factory 46 | audit_map_file = /etc/nova/api_audit_map.conf 47 | 48 | The filter should be included after Keystone middleware's auth_token middleware 49 | so it can utilise environment variables set by auth_token. Below is an example 50 | using Nova's WSGI pipeline:: 51 | 52 | [composite:openstack_compute_api_v2] 53 | use = call:nova.api.auth:pipeline_factory 54 | noauth = faultwrap sizelimit noauth ratelimit osapi_compute_app_v2 55 | keystone = faultwrap sizelimit authtoken keystonecontext ratelimit audit osapi_compute_app_v2 56 | keystone_nolimit = faultwrap sizelimit authtoken keystonecontext audit osapi_compute_app_v2 57 | 58 | .. _oslo.messaging: http://www.github.com/openstack/oslo.messaging 59 | 60 | Configure audit middleware 61 | ========================== 62 | To properly audit api requests, the audit middleware requires an 63 | api_audit_map.conf to be defined. The project's corresponding 64 | api_audit_map.conf file is included in the `pyCADF library`_. 65 | 66 | The location of the mapping file should be specified explicitly by adding the 67 | path to the 'audit_map_file' option of the filter definition:: 68 | 69 | [filter:audit] 70 | paste.filter_factory = keystonemiddleware.audit:filter_factory 71 | audit_map_file = /etc/nova/api_audit_map.conf 72 | 73 | Additional options can be set:: 74 | 75 | [filter:audit] 76 | paste.filter_factory = pycadf.middleware.audit:filter_factory 77 | audit_map_file = /etc/nova/api_audit_map.conf 78 | service_name = test # opt to set HTTP_X_SERVICE_NAME environ variable 79 | ignore_req_list = GET,POST # opt to ignore specific requests 80 | 81 | Audit middleware can be configured to use its own exclusive notification driver 82 | and topic(s) value. This can be useful when the service is already using oslo 83 | messaging notifications and wants to use a different driver for auditing e.g. 84 | service has existing notifications sent to queue via 'messagingv2' and wants to 85 | send audit notifications to a log file via 'log' driver. Example shown below:: 86 | 87 | [audit_middleware_notifications] 88 | driver = log 89 | 90 | When audit events are sent via 'messagingv2' or 'messaging', middleware can 91 | specify a transport URL if its transport URL needs to be different from the 92 | service's own messaging transport setting. Other Transport related settings are 93 | read from oslo messaging sections defined in service configuration e.g. 94 | 'oslo_messaging_rabbit'. Example shown below:: 95 | 96 | [audit_middleware_notifications] 97 | driver = messaging 98 | transport_url = rabbit://user2:passwd@host:5672/another_virtual_host 99 | 100 | .. _pyCADF library: https://github.com/openstack/pycadf/tree/master/etc/pycadf 101 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/auth_token/test_auth.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import io 14 | import uuid 15 | 16 | from keystoneauth1 import fixture 17 | from keystoneauth1 import plugin as ksa_plugin 18 | from keystoneauth1 import session 19 | from oslo_log import log as logging 20 | from requests_mock.contrib import fixture as rm_fixture 21 | 22 | from keystonemiddleware.auth_token import _auth 23 | from keystonemiddleware.tests.unit import utils 24 | 25 | 26 | class DefaultAuthPluginTests(utils.BaseTestCase): 27 | 28 | def new_plugin(self, auth_host=None, auth_port=None, auth_protocol=None, 29 | auth_admin_prefix=None, admin_user=None, 30 | admin_password=None, admin_tenant_name=None, 31 | admin_token=None, identity_uri=None, log=None): 32 | if not log: 33 | log = self.logger 34 | 35 | return _auth.AuthTokenPlugin( 36 | auth_host=auth_host, 37 | auth_port=auth_port, 38 | auth_protocol=auth_protocol, 39 | auth_admin_prefix=auth_admin_prefix, 40 | admin_user=admin_user, 41 | admin_password=admin_password, 42 | admin_tenant_name=admin_tenant_name, 43 | admin_token=admin_token, 44 | identity_uri=identity_uri, 45 | log=log) 46 | 47 | def setUp(self): 48 | super(DefaultAuthPluginTests, self).setUp() 49 | 50 | self.stream = io.StringIO() 51 | self.logger = logging.getLogger(__name__) 52 | self.session = session.Session() 53 | self.requests_mock = self.useFixture(rm_fixture.Fixture()) 54 | 55 | def test_auth_uri_from_fragments(self): 56 | auth_protocol = 'http' 57 | auth_host = 'testhost' 58 | auth_port = 8888 59 | auth_admin_prefix = 'admin' 60 | 61 | expected = '%s://%s:%d/admin' % (auth_protocol, auth_host, auth_port) 62 | 63 | plugin = self.new_plugin(auth_host=auth_host, 64 | auth_protocol=auth_protocol, 65 | auth_port=auth_port, 66 | auth_admin_prefix=auth_admin_prefix) 67 | 68 | endpoint = plugin.get_endpoint(self.session, 69 | interface=ksa_plugin.AUTH_INTERFACE) 70 | self.assertEqual(expected, endpoint) 71 | 72 | def test_identity_uri_overrides_fragments(self): 73 | identity_uri = 'http://testhost:8888/admin' 74 | plugin = self.new_plugin(identity_uri=identity_uri, 75 | auth_host='anotherhost', 76 | auth_port=9999, 77 | auth_protocol='ftp') 78 | 79 | endpoint = plugin.get_endpoint(self.session, 80 | interface=ksa_plugin.AUTH_INTERFACE) 81 | self.assertEqual(identity_uri, endpoint) 82 | 83 | def test_with_admin_token(self): 84 | token = uuid.uuid4().hex 85 | plugin = self.new_plugin(identity_uri='http://testhost:8888/admin', 86 | admin_token=token) 87 | self.assertEqual(token, plugin.get_token(self.session)) 88 | 89 | def test_with_user_pass(self): 90 | base_uri = 'http://testhost:8888/admin' 91 | token = fixture.V2Token() 92 | admin_tenant_name = uuid.uuid4().hex 93 | 94 | self.requests_mock.post(base_uri + '/v2.0/tokens', 95 | json=token) 96 | 97 | plugin = self.new_plugin(identity_uri=base_uri, 98 | admin_user=uuid.uuid4().hex, 99 | admin_password=uuid.uuid4().hex, 100 | admin_tenant_name=admin_tenant_name) 101 | 102 | self.assertEqual(token.token_id, plugin.get_token(self.session)) 103 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/audit/test_audit_oslo_messaging.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | from unittest import mock 14 | 15 | from keystonemiddleware import audit 16 | from keystonemiddleware.tests.unit.audit import base 17 | 18 | 19 | class AuditNotifierConfigTest(base.BaseAuditMiddlewareTest): 20 | 21 | def test_conf_middleware_log_and_default_as_messaging(self): 22 | self.cfg.config(driver='log', 23 | group='audit_middleware_notifications') 24 | app = self.create_simple_app() 25 | with mock.patch('oslo_messaging.notify._impl_log.LogDriver.notify', 26 | side_effect=Exception('error')) as driver: 27 | app.get('/foo/bar', extra_environ=self.get_environ_header()) 28 | # audit middleware conf has 'log' make sure that driver is invoked 29 | # and not the one specified in DEFAULT section 30 | self.assertTrue(driver.called) 31 | 32 | def test_conf_middleware_log_and_oslo_msg_as_messaging(self): 33 | self.cfg.config(driver=['messaging'], 34 | group='oslo_messaging_notifications') 35 | self.cfg.config(driver='log', 36 | group='audit_middleware_notifications') 37 | 38 | app = self.create_simple_app() 39 | with mock.patch('oslo_messaging.notify._impl_log.LogDriver.notify', 40 | side_effect=Exception('error')) as driver: 41 | app.get('/foo/bar', extra_environ=self.get_environ_header()) 42 | # audit middleware conf has 'log' make sure that driver is invoked 43 | # and not the one specified in oslo_messaging_notifications section 44 | self.assertTrue(driver.called) 45 | 46 | def test_conf_middleware_messaging_and_oslo_msg_as_log(self): 47 | self.cfg.config(driver=['log'], group='oslo_messaging_notifications') 48 | self.cfg.config(driver='messaging', 49 | group='audit_middleware_notifications') 50 | app = self.create_simple_app() 51 | with mock.patch('oslo_messaging.notify.messaging.MessagingDriver' 52 | '.notify', 53 | side_effect=Exception('error')) as driver: 54 | # audit middleware has 'messaging' make sure that driver is invoked 55 | # and not the one specified in oslo_messaging_notifications section 56 | app.get('/foo/bar', extra_environ=self.get_environ_header()) 57 | self.assertTrue(driver.called) 58 | 59 | def test_with_no_middleware_notification_conf(self): 60 | self.cfg.config(driver=['messaging'], 61 | group='oslo_messaging_notifications') 62 | self.cfg.config(driver=None, group='audit_middleware_notifications') 63 | 64 | app = self.create_simple_app() 65 | with mock.patch('oslo_messaging.notify.messaging.MessagingDriver' 66 | '.notify', 67 | side_effect=Exception('error')) as driver: 68 | # audit middleware section is not set. So driver needs to be 69 | # invoked from oslo_messaging_notifications section. 70 | app.get('/foo/bar', extra_environ=self.get_environ_header()) 71 | self.assertTrue(driver.called) 72 | 73 | @mock.patch('oslo_messaging.get_notification_transport') 74 | def test_conf_middleware_messaging_and_transport_set(self, m): 75 | transport_url = 'rabbit://me:passwd@host:5672/virtual_host' 76 | self.cfg.config(driver='messaging', 77 | transport_url=transport_url, 78 | group='audit_middleware_notifications') 79 | 80 | self.create_simple_middleware() 81 | self.assertTrue(m.called) 82 | # make sure first call kwarg 'url' is same as provided transport_url 83 | self.assertEqual(transport_url, m.call_args_list[0][1]['url']) 84 | 85 | def test_do_not_use_oslo_messaging(self): 86 | self.cfg.config(use_oslo_messaging=False, 87 | group='audit_middleware_notifications') 88 | audit_middleware = self.create_simple_middleware() 89 | 90 | # make sure it is using a local notifier instead of oslo_messaging 91 | self.assertIsInstance(audit_middleware._notifier, 92 | audit._notifier._LogNotifier) 93 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/auth_token/test_connection_pool.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import queue 14 | import time 15 | from unittest import mock 16 | 17 | from oslo_cache import _memcache_pool 18 | import testtools 19 | from testtools import matchers 20 | 21 | from keystonemiddleware.tests.unit import utils 22 | 23 | 24 | class _TestConnectionPool(_memcache_pool.ConnectionPool): 25 | destroyed_value = 'destroyed' 26 | 27 | def _create_connection(self): 28 | return mock.MagicMock() 29 | 30 | def _destroy_connection(self, conn): 31 | conn(self.destroyed_value) 32 | 33 | 34 | class TestConnectionPool(utils.TestCase): 35 | def setUp(self): 36 | super(TestConnectionPool, self).setUp() 37 | self.unused_timeout = 10 38 | self.maxsize = 2 39 | self.connection_pool = _TestConnectionPool( 40 | maxsize=self.maxsize, 41 | unused_timeout=self.unused_timeout) 42 | 43 | def test_get_context_manager(self): 44 | self.assertThat(self.connection_pool.queue, matchers.HasLength(0)) 45 | with self.connection_pool.acquire() as conn: 46 | self.assertEqual(1, self.connection_pool._acquired) 47 | self.assertEqual(0, self.connection_pool._acquired) 48 | self.assertThat(self.connection_pool.queue, matchers.HasLength(1)) 49 | self.assertEqual(conn, self.connection_pool.queue[0].connection) 50 | 51 | def test_cleanup_pool(self): 52 | self.test_get_context_manager() 53 | newtime = time.time() + self.unused_timeout * 2 54 | non_expired_connection = _memcache_pool._PoolItem( 55 | ttl=(newtime * 2), 56 | connection=mock.MagicMock()) 57 | self.connection_pool.queue.append(non_expired_connection) 58 | self.assertThat(self.connection_pool.queue, matchers.HasLength(2)) 59 | with mock.patch.object(time, 'time', return_value=newtime): 60 | conn = self.connection_pool.queue[0].connection 61 | with self.connection_pool.acquire(): 62 | pass 63 | conn.assert_has_calls( 64 | [mock.call(self.connection_pool.destroyed_value)]) 65 | self.assertThat(self.connection_pool.queue, matchers.HasLength(1)) 66 | self.assertEqual(0, non_expired_connection.connection.call_count) 67 | 68 | def test_acquire_conn_exception_returns_acquired_count(self): 69 | class TestException(Exception): 70 | pass 71 | 72 | with mock.patch.object(_TestConnectionPool, '_create_connection', 73 | side_effect=TestException): 74 | with testtools.ExpectedException(TestException): 75 | with self.connection_pool.acquire(): 76 | pass 77 | self.assertThat(self.connection_pool.queue, 78 | matchers.HasLength(0)) 79 | self.assertEqual(0, self.connection_pool._acquired) 80 | 81 | def test_connection_pool_limits_maximum_connections(self): 82 | # NOTE(morganfainberg): To ensure we don't lockup tests until the 83 | # job limit, explicitly call .get_nowait() and .put_nowait() in this 84 | # case. 85 | conn1 = self.connection_pool.get_nowait() 86 | conn2 = self.connection_pool.get_nowait() 87 | 88 | # Use a nowait version to raise an Empty exception indicating we would 89 | # not get another connection until one is placed back into the queue. 90 | self.assertRaises(queue.Empty, self.connection_pool.get_nowait) 91 | 92 | # Place the connections back into the pool. 93 | self.connection_pool.put_nowait(conn1) 94 | self.connection_pool.put_nowait(conn2) 95 | 96 | # Make sure we can get a connection out of the pool again. 97 | self.connection_pool.get_nowait() 98 | 99 | def test_connection_pool_maximum_connection_get_timeout(self): 100 | connection_pool = _TestConnectionPool( 101 | maxsize=1, 102 | unused_timeout=self.unused_timeout, 103 | conn_get_timeout=0) 104 | 105 | def _acquire_connection(): 106 | with connection_pool.acquire(): 107 | pass 108 | 109 | # Make sure we've consumed the only available connection from the pool 110 | conn = connection_pool.get_nowait() 111 | 112 | self.assertRaises(_memcache_pool.exception.QueueEmpty, 113 | _acquire_connection) 114 | 115 | # Put the connection back and ensure we can acquire the connection 116 | # after it is available. 117 | connection_pool.put_nowait(conn) 118 | _acquire_connection() 119 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/auth_token/test_config.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | from unittest import mock 14 | import uuid 15 | 16 | from oslo_config import cfg 17 | from oslotest import createfile 18 | 19 | from keystonemiddleware.auth_token import _auth 20 | from keystonemiddleware.auth_token import _opts 21 | from keystonemiddleware.tests.unit.auth_token import base 22 | 23 | 24 | def conf_get(app, *args, **kwargs): 25 | return app._conf.get(*args, **kwargs) 26 | 27 | 28 | class TestAuthPluginLocalOsloConfig(base.BaseAuthTokenTestCase): 29 | 30 | def setUp(self): 31 | super(TestAuthPluginLocalOsloConfig, self).setUp() 32 | self.project = uuid.uuid4().hex 33 | 34 | # NOTE(cdent): The options below are selected from those 35 | # which are statically registered by auth_token middleware 36 | # in the 'keystone_authtoken' group. Additional options, from 37 | # plugins, are registered dynamically so must not be used here. 38 | self.oslo_options = { 39 | 'www_authenticate_uri': uuid.uuid4().hex, 40 | 'identity_uri': uuid.uuid4().hex, 41 | } 42 | 43 | self.local_oslo_config = cfg.ConfigOpts() 44 | self.local_oslo_config.register_group( 45 | cfg.OptGroup(name='keystone_authtoken')) 46 | 47 | self.local_oslo_config.register_opts(_opts._OPTS, 48 | group='keystone_authtoken') 49 | self.local_oslo_config.register_opts(_auth.OPTS, 50 | group='keystone_authtoken') 51 | 52 | for option, value in self.oslo_options.items(): 53 | self.local_oslo_config.set_override(option, value, 54 | 'keystone_authtoken') 55 | self.local_oslo_config(args=[], project=self.project) 56 | 57 | self.file_options = { 58 | 'auth_type': 'password', 59 | 'www_authenticate_uri': uuid.uuid4().hex, 60 | 'password': uuid.uuid4().hex, 61 | } 62 | 63 | content = ("[DEFAULT]\n" 64 | "test_opt=15\n" 65 | "[keystone_authtoken]\n" 66 | "auth_type=%(auth_type)s\n" 67 | "www_authenticate_uri=%(www_authenticate_uri)s\n" 68 | "auth_url=%(www_authenticate_uri)s\n" 69 | "password=%(password)s\n" % self.file_options) 70 | 71 | self.conf_file_fixture = self.useFixture( 72 | createfile.CreateFileWithContent(self.project, content)) 73 | 74 | def _create_app(self, conf, project_version=None): 75 | if not project_version: 76 | project_version = uuid.uuid4().hex 77 | 78 | fake_im = mock.Mock() 79 | fake_im.return_value = project_version 80 | 81 | body = uuid.uuid4().hex 82 | at_im = 'keystonemiddleware._common.config.importlib.metadata.version' 83 | with mock.patch(at_im, new=fake_im): 84 | # use_global_conf is poorly named. What it means is 85 | # don't use the config created in test setUp. 86 | return self.create_simple_middleware(body=body, conf=conf, 87 | use_global_conf=True) 88 | 89 | def test_project_in_local_oslo_configuration(self): 90 | conf = {'oslo_config_project': self.project, 91 | 'oslo_config_file': self.conf_file_fixture.path} 92 | app = self._create_app(conf) 93 | for option in self.file_options: 94 | self.assertEqual(self.file_options[option], 95 | conf_get(app, option), option) 96 | 97 | def test_passed_oslo_configuration(self): 98 | conf = {'oslo_config_config': self.local_oslo_config} 99 | app = self._create_app(conf) 100 | for option in self.oslo_options: 101 | self.assertEqual(self.oslo_options[option], 102 | conf_get(app, option)) 103 | 104 | def test_passed_oslo_configuration_with_deprecated_ones(self): 105 | deprecated_opt = cfg.IntOpt('test_opt', deprecated_for_removal=True) 106 | cfg.CONF.register_opt(deprecated_opt) 107 | cfg.CONF(args=[], 108 | default_config_files=[self.conf_file_fixture.path]) 109 | conf = {'oslo_config_config': cfg.CONF} 110 | 111 | # success to init AuthProtocol 112 | self._create_app(conf) 113 | 114 | def test_passed_oslo_configuration_wins(self): 115 | """oslo_config_config has precedence over oslo_config_project.""" 116 | conf = {'oslo_config_project': self.project, 117 | 'oslo_config_config': self.local_oslo_config, 118 | 'oslo_config_file': self.conf_file_fixture.path} 119 | app = self._create_app(conf) 120 | for option in self.oslo_options: 121 | self.assertEqual(self.oslo_options[option], 122 | conf_get(app, option)) 123 | self.assertNotEqual(self.file_options['www_authenticate_uri'], 124 | conf_get(app, 'www_authenticate_uri')) 125 | -------------------------------------------------------------------------------- /keystonemiddleware/locale/en_GB/LC_MESSAGES/keystonemiddleware.po: -------------------------------------------------------------------------------- 1 | # Andi Chandler , 2017. #zanata 2 | # Andi Chandler , 2024. #zanata 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: keystonemiddleware VERSION\n" 6 | "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" 7 | "POT-Creation-Date: 2024-06-28 17:34+0000\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "PO-Revision-Date: 2024-07-18 10:19+0000\n" 12 | "Last-Translator: Andi Chandler \n" 13 | "Language-Team: English (United Kingdom)\n" 14 | "Language: en_GB\n" 15 | "X-Generator: Zanata 4.3.3\n" 16 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 17 | 18 | msgid "Access key not provided" 19 | msgstr "Access key not provided" 20 | 21 | msgid "An exception occurred during the token verification process." 22 | msgstr "An exception occurred during the token verification process." 23 | 24 | msgid "Can not generate the thumbprint." 25 | msgstr "Can not generate the thumbprint." 26 | 27 | msgid "Configuration error. Failed to read the JWT key file." 28 | msgstr "Configuration error. Failed to read the JWT key file." 29 | 30 | msgid "" 31 | "Configuration error. JWT encoding with the specified JWT key file and " 32 | "algorithm failed." 33 | msgstr "" 34 | "Configuration error. JWT encoding with the specified JWT key file and " 35 | "algorithm failed." 36 | 37 | msgid "" 38 | "Configuration error. JWT encoding with the specified client_secret and " 39 | "algorithm failed." 40 | msgstr "" 41 | "Configuration error. JWT encoding with the specified client_secret and " 42 | "algorithm failed." 43 | 44 | msgid "Configuration error. JWT key file is not a file." 45 | msgstr "Configuration error. JWT key file is not a file." 46 | 47 | msgid "Configuration error. The JWT key file content is empty." 48 | msgstr "Configuration error. The JWT key file content is empty." 49 | 50 | #, python-format 51 | msgid "Configuration error. The parameter is not set for \"%s\" in group [%s]." 52 | msgstr "" 53 | "Configuration error. The parameter is not set for \"%s\" in group [%s]." 54 | 55 | msgid "Encrypted data appears to be corrupted." 56 | msgstr "Encrypted data appears to be corrupted." 57 | 58 | #, python-format 59 | msgid "Error response from keystone: %s" 60 | msgstr "Error response from Keystone: %s" 61 | 62 | msgid "Failed to fetch token data from identity server" 63 | msgstr "Failed to fetch token data from identity server" 64 | 65 | #, python-format 66 | msgid "Failed to parse the necessary information for the field \"%s\"." 67 | msgstr "Failed to parse the necessary information for the field \"%s\"." 68 | 69 | msgid "Failure parsing response from keystone" 70 | msgstr "Failure parsing response from Keystone" 71 | 72 | msgid "Identity server rejected authorization necessary to fetch token data" 73 | msgstr "Identity server rejected authorisation necessary to fetch token data" 74 | 75 | msgid "Invalid MAC; data appears to be corrupted." 76 | msgstr "Invalid MAC; data appears to be corrupted." 77 | 78 | msgid "Invalid version asked for in auth_token plugin" 79 | msgstr "Invalid version asked for in auth_token plugin" 80 | 81 | msgid "No compatible apis supported by server" 82 | msgstr "No compatible APIs supported by server" 83 | 84 | msgid "Signature not provided" 85 | msgstr "Signature not provided" 86 | 87 | msgid "The Introspect API service is temporarily unavailable." 88 | msgstr "The Introspect API service is temporarily unavailable." 89 | 90 | #, python-format 91 | msgid "" 92 | "The configuration parameter for key \"auth_method\" in group [%s] is " 93 | "incorrect." 94 | msgstr "" 95 | "The configuration parameter for key \"auth_method\" in group [%s] is " 96 | "incorrect." 97 | 98 | msgid "" 99 | "The request you have made is denied, because an exception occurred while " 100 | "accessing the external authentication server for token validation." 101 | msgstr "" 102 | "The request you have made is denied, because an exception occurred while " 103 | "accessing the external authentication server for token validation." 104 | 105 | msgid "" 106 | "The request you have made is denied, because the configuration parameters " 107 | "are incorrect and the token can not be verified." 108 | msgstr "" 109 | "The request you have made is denied, because the configuration parameters " 110 | "are incorrect and the token can not be verified." 111 | 112 | msgid "" 113 | "The request you have made is denied, because the necessary information could " 114 | "not be parsed." 115 | msgstr "" 116 | "The request you have made is denied, because the necessary information could " 117 | "not be parsed." 118 | 119 | msgid "The request you have made is denied, because the token is invalid." 120 | msgstr "The request you have made is denied, because the token is invalid." 121 | 122 | msgid "The request you have made requires authentication." 123 | msgstr "The request you have made requires authentication." 124 | 125 | msgid "The token cannot be verified for validity." 126 | msgstr "The token cannot be verified for validity." 127 | 128 | msgid "The token is invalid." 129 | msgstr "The token is invalid." 130 | 131 | msgid "The two thumbprints do not match." 132 | msgstr "The two thumbprints do not match." 133 | 134 | msgid "Token authorization failed" 135 | msgstr "Token authorisation failed" 136 | 137 | msgid "Unable to determine service tenancy." 138 | msgstr "Unable to determine service tenancy." 139 | 140 | msgid "Unable to obtain the access token." 141 | msgstr "Unable to obtain the access token." 142 | 143 | msgid "Unable to obtain the client certificate." 144 | msgstr "Unable to obtain the client certificate." 145 | 146 | msgid "" 147 | "memcache_secret_key must be defined when a memcache_security_strategy is " 148 | "defined" 149 | msgstr "" 150 | "memcache_secret_key must be defined when a memcache_security_strategy is " 151 | "defined" 152 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/utils.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import sys 14 | import time 15 | from unittest import mock 16 | import uuid 17 | import warnings 18 | 19 | import fixtures 20 | from oslo_log import log as logging 21 | import oslotest.base as oslotest 22 | import requests 23 | import webob 24 | import webtest 25 | 26 | 27 | class BaseTestCase(oslotest.BaseTestCase): 28 | def setUp(self): 29 | super(BaseTestCase, self).setUp() 30 | 31 | # If keystonemiddleware calls any deprecated function this will raise 32 | # an exception. 33 | warnings.filterwarnings('error', category=DeprecationWarning, 34 | module='^keystonemiddleware\\.') 35 | self.addCleanup(warnings.resetwarnings) 36 | 37 | self.logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) 38 | 39 | 40 | class TestCase(BaseTestCase): 41 | TEST_DOMAIN_ID = '1' 42 | TEST_DOMAIN_NAME = 'aDomain' 43 | TEST_GROUP_ID = uuid.uuid4().hex 44 | TEST_ROLE_ID = uuid.uuid4().hex 45 | TEST_TENANT_ID = '1' 46 | TEST_TENANT_NAME = 'aTenant' 47 | TEST_TOKEN = 'aToken' 48 | TEST_TRUST_ID = 'aTrust' 49 | TEST_USER = 'test' 50 | TEST_USER_ID = uuid.uuid4().hex 51 | 52 | TEST_ROOT_URL = 'http://127.0.0.1:5000/' 53 | 54 | def setUp(self): 55 | super(TestCase, self).setUp() 56 | self.time_patcher = mock.patch.object(time, 'time', lambda: 1234) 57 | self.time_patcher.start() 58 | 59 | def tearDown(self): 60 | self.time_patcher.stop() 61 | super(TestCase, self).tearDown() 62 | 63 | 64 | class MiddlewareTestCase(BaseTestCase): 65 | 66 | def create_middleware(self, cb, **kwargs): 67 | raise NotImplementedError("implement this in your tests") 68 | 69 | def create_simple_middleware(self, 70 | status='200 OK', 71 | body='', 72 | headers=None, 73 | **kwargs): 74 | def cb(req): 75 | resp = webob.Response(body, status) 76 | resp.headers.update(headers or {}) 77 | return resp 78 | 79 | return self.create_middleware(cb, **kwargs) 80 | 81 | def create_app(self, *args, **kwargs): 82 | return webtest.TestApp(self.create_middleware(*args, **kwargs)) 83 | 84 | def create_simple_app(self, *args, **kwargs): 85 | return webtest.TestApp(self.create_simple_middleware(*args, **kwargs)) 86 | 87 | 88 | class TestResponse(requests.Response): 89 | """Utility class to wrap requests.Response. 90 | 91 | Class used to wrap requests.Response and provide some convenience to 92 | initialize with a dict. 93 | """ 94 | 95 | def __init__(self, data): 96 | self._text = None 97 | super(TestResponse, self).__init__() 98 | if isinstance(data, dict): 99 | self.status_code = data.get('status_code', 200) 100 | headers = data.get('headers') 101 | if headers: 102 | self.headers.update(headers) 103 | # Fake the text attribute to streamline Response creation 104 | # _content is defined by requests.Response 105 | self._content = data.get('text') 106 | else: 107 | self.status_code = data 108 | 109 | def __eq__(self, other): 110 | """Test if the response is equivalent to another response. 111 | 112 | This works by comparing the attribute dictionaries of both TestResponse 113 | instances. 114 | """ 115 | return self.__dict__ == other.__dict__ 116 | 117 | @property 118 | def text(self): 119 | return self.content 120 | 121 | 122 | class DisableModuleFixture(fixtures.Fixture): 123 | """A fixture to provide support for unloading/disabling modules.""" 124 | 125 | def __init__(self, module, *args, **kw): 126 | super(DisableModuleFixture, self).__init__(*args, **kw) 127 | self.module = module 128 | self._finders = [] 129 | self._cleared_modules = {} 130 | 131 | def tearDown(self): 132 | super(DisableModuleFixture, self).tearDown() 133 | for finder in self._finders: 134 | sys.meta_path.remove(finder) 135 | sys.modules.update(self._cleared_modules) 136 | 137 | def clear_module(self): 138 | cleared_modules = {} 139 | for fullname in list(sys.modules.keys()): 140 | if (fullname == self.module or 141 | fullname.startswith(self.module + '.')): 142 | cleared_modules[fullname] = sys.modules.pop(fullname) 143 | return cleared_modules 144 | 145 | def setUp(self): 146 | """Ensure ImportError for the specified module.""" 147 | super(DisableModuleFixture, self).setUp() 148 | 149 | # Clear 'module' references in sys.modules 150 | self._cleared_modules.update(self.clear_module()) 151 | 152 | finder = NoModuleFinder(self.module) 153 | self._finders.append(finder) 154 | sys.meta_path.insert(0, finder) 155 | 156 | 157 | class NoModuleFinder(object): 158 | """Disallow further imports of 'module'.""" 159 | 160 | def __init__(self, module): 161 | self.module = module 162 | 163 | def find_module(self, fullname, path): 164 | if fullname == self.module or fullname.startswith(self.module + '.'): 165 | raise ImportError 166 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/test_opts.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 OpenStack Foundation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import stevedore 16 | from testtools import matchers 17 | 18 | from keystonemiddleware.auth_token import _opts as new_opts 19 | from keystonemiddleware import opts as old_opts 20 | from keystonemiddleware.tests.unit import utils 21 | 22 | 23 | class OptsTestCase(utils.TestCase): 24 | 25 | def test_original_list_all_options(self): 26 | result_of_old_opts = old_opts.list_auth_token_opts() 27 | self.assertThat(result_of_old_opts, matchers.HasLength(1)) 28 | 29 | for group in (g for (g, _l) in result_of_old_opts): 30 | self.assertEqual('keystone_authtoken', group) 31 | 32 | # This is the original list that includes deprecated options 33 | expected_opt_names = [ 34 | 'auth_admin_prefix', 35 | 'auth_host', 36 | 'interface', 37 | 'auth_port', 38 | 'auth_protocol', 39 | 'www_authenticate_uri', 40 | 'auth_uri', 41 | 'identity_uri', 42 | 'auth_version', 43 | 'delay_auth_decision', 44 | 'http_connect_timeout', 45 | 'http_request_max_retries', 46 | 'admin_token', 47 | 'admin_user', 48 | 'admin_password', 49 | 'admin_tenant_name', 50 | 'cache', 51 | 'certfile', 52 | 'keyfile', 53 | 'cafile', 54 | 'region_name', 55 | 'insecure', 56 | 'memcached_servers', 57 | 'token_cache_time', 58 | 'memcache_security_strategy', 59 | 'memcache_secret_key', 60 | 'memcache_use_advanced_pool', 61 | 'memcache_tls_enabled', 62 | 'memcache_tls_cafile', 63 | 'memcache_tls_certfile', 64 | 'memcache_tls_keyfile', 65 | 'memcache_tls_allowed_ciphers', 66 | 'memcache_pool_dead_retry', 67 | 'memcache_pool_maxsize', 68 | 'memcache_pool_unused_timeout', 69 | 'memcache_pool_conn_get_timeout', 70 | 'memcache_pool_socket_timeout', 71 | 'memcache_sasl_enabled', 72 | 'memcache_username', 73 | 'memcache_password', 74 | 'include_service_catalog', 75 | 'enforce_token_bind', 76 | 'auth_type', 77 | 'auth_section', 78 | 'service_token_roles', 79 | 'service_token_roles_required', 80 | 'service_type', 81 | ] 82 | opt_names = [o.name for (g, l) in result_of_old_opts for o in l] 83 | self.assertThat(opt_names, matchers.HasLength(len(expected_opt_names))) 84 | 85 | for opt in opt_names: 86 | self.assertIn(opt, expected_opt_names) 87 | 88 | def _test_list_auth_token_opts(self, result): 89 | self.assertThat(result, matchers.HasLength(1)) 90 | 91 | for group in (g for (g, _l) in result): 92 | self.assertEqual('keystone_authtoken', group) 93 | 94 | # This is the sample config generator list WITHOUT deprecations 95 | expected_opt_names = [ 96 | 'www_authenticate_uri', 97 | 'interface', 98 | 'auth_uri', 99 | 'auth_version', 100 | 'delay_auth_decision', 101 | 'http_connect_timeout', 102 | 'http_request_max_retries', 103 | 'cache', 104 | 'certfile', 105 | 'keyfile', 106 | 'cafile', 107 | 'region_name', 108 | 'insecure', 109 | 'memcached_servers', 110 | 'token_cache_time', 111 | 'memcache_security_strategy', 112 | 'memcache_secret_key', 113 | 'memcache_use_advanced_pool', 114 | 'memcache_tls_enabled', 115 | 'memcache_tls_cafile', 116 | 'memcache_tls_certfile', 117 | 'memcache_tls_keyfile', 118 | 'memcache_tls_allowed_ciphers', 119 | 'memcache_pool_dead_retry', 120 | 'memcache_pool_maxsize', 121 | 'memcache_pool_unused_timeout', 122 | 'memcache_pool_conn_get_timeout', 123 | 'memcache_pool_socket_timeout', 124 | 'memcache_sasl_enabled', 125 | 'memcache_username', 126 | 'memcache_password', 127 | 'include_service_catalog', 128 | 'enforce_token_bind', 129 | 'auth_type', 130 | 'auth_section', 131 | 'service_token_roles', 132 | 'service_token_roles_required', 133 | 'service_type', 134 | ] 135 | opt_names = [o.name for (g, l) in result for o in l] 136 | self.assertThat(opt_names, matchers.HasLength(len(expected_opt_names))) 137 | 138 | for opt in opt_names: 139 | self.assertIn(opt, expected_opt_names) 140 | 141 | def test_list_auth_token_opts(self): 142 | self._test_list_auth_token_opts(new_opts.list_opts()) 143 | 144 | def test_entry_point(self): 145 | em = stevedore.ExtensionManager('oslo.config.opts', 146 | invoke_on_load=True) 147 | for extension in em: 148 | if extension.name == 'keystonemiddleware.auth_token': 149 | break 150 | else: 151 | self.fail('keystonemiddleware.auth_token not found') 152 | 153 | self._test_list_auth_token_opts(extension.obj) 154 | -------------------------------------------------------------------------------- /keystonemiddleware/_common/config.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import importlib.metadata 14 | 15 | from oslo_config import cfg 16 | from oslo_log import log as logging 17 | import pbr 18 | 19 | from keystonemiddleware import exceptions 20 | from keystonemiddleware.i18n import _ 21 | 22 | CONF = cfg.CONF 23 | _NOT_SET = object() 24 | _LOG = logging.getLogger(__name__) 25 | 26 | 27 | def _conf_values_type_convert(group_name, all_options, conf): 28 | """Convert conf values into correct type.""" 29 | if not conf: 30 | return {} 31 | 32 | opts = {} 33 | opt_types = {} 34 | 35 | for group, options in all_options: 36 | # only accept paste overrides for the primary group 37 | if group != group_name: 38 | continue 39 | 40 | for o in options: 41 | type_dest = (getattr(o, 'type', str), o.dest) 42 | opt_types[o.dest] = type_dest 43 | # Also add the deprecated name with the same type and dest. 44 | for d_o in o.deprecated_opts: 45 | opt_types[d_o.name] = type_dest 46 | 47 | break 48 | 49 | for k, v in conf.items(): 50 | dest = k 51 | try: 52 | # 'here' and '__file__' come from paste.deploy 53 | # 'configkey' is added by panko and gnocchi 54 | if v is not None and k not in ['here', '__file__', 'configkey']: 55 | type_, dest = opt_types[k] 56 | v = type_(v) 57 | except KeyError: # nosec 58 | _LOG.warning( 59 | 'The option "%s" is not known to keystonemiddleware', k) 60 | except ValueError as e: 61 | raise exceptions.ConfigurationError( 62 | _('Unable to convert the value of option "%(key)s" into ' 63 | 'correct type: %(ex)s') % {'key': k, 'ex': e}) 64 | opts[dest] = v 65 | 66 | return opts 67 | 68 | 69 | class Config(object): 70 | 71 | def __init__(self, name, group_name, all_options, conf): 72 | local_oslo_config = conf.pop('oslo_config_config', None) 73 | local_config_project = conf.pop('oslo_config_project', None) 74 | local_config_file = conf.pop('oslo_config_file', None) 75 | 76 | # NOTE(wanghong): If options are set in paste file, all the option 77 | # values passed into conf are string type. So, we should convert the 78 | # conf value into correct type. 79 | self.paste_overrides = _conf_values_type_convert(group_name, 80 | all_options, 81 | conf) 82 | 83 | # NOTE(sileht, cdent): If we don't want to use oslo.config global 84 | # object there are two options: set "oslo_config_project" in 85 | # paste.ini and the middleware will load the configuration with a 86 | # local oslo.config object or the caller which instantiates 87 | # AuthProtocol can pass in an existing oslo.config as the 88 | # value of the "oslo_config_config" key in conf. If both are 89 | # set "oslo_config_config" is used. 90 | if local_config_project and not local_oslo_config: 91 | config_files = [local_config_file] if local_config_file else None 92 | 93 | local_oslo_config = cfg.ConfigOpts() 94 | local_oslo_config([], 95 | project=local_config_project, 96 | default_config_files=config_files, 97 | validate_default_values=True) 98 | 99 | if local_oslo_config: 100 | for group, opts in all_options: 101 | local_oslo_config.register_opts(opts, group=group) 102 | 103 | self.name = name 104 | self.oslo_conf_obj = local_oslo_config or cfg.CONF 105 | self.group_name = group_name 106 | self._user_agent = None 107 | 108 | def get(self, name, group=_NOT_SET): 109 | # try config from paste-deploy first 110 | try: 111 | return self.paste_overrides[name] 112 | except KeyError: 113 | if group is _NOT_SET: 114 | group = self.group_name 115 | 116 | return self.oslo_conf_obj[group][name] 117 | 118 | @property 119 | def project(self): 120 | """Determine a project name from all available config sources. 121 | 122 | The sources are checked in the following order: 123 | 124 | 1. The paste-deploy config for auth_token middleware 125 | 2. The keystone_authtoken or base group in the project's config 126 | 3. The oslo.config CONF.project property 127 | 128 | """ 129 | try: 130 | return self.get('project', group=self.group_name) 131 | except cfg.NoSuchOptError: 132 | try: 133 | # CONF.project will exist only if the service uses 134 | # oslo.config. It will only be set when the project 135 | # calls CONF(...) and when not set oslo.config oddly 136 | # raises a NoSuchOptError exception. 137 | return self.oslo_conf_obj.project 138 | except cfg.NoSuchOptError: 139 | return None 140 | 141 | @property 142 | def user_agent(self): 143 | if not self._user_agent: 144 | project = self.project or '' 145 | 146 | if project: 147 | try: 148 | version = importlib.metadata.version(project) 149 | except importlib.metadata.PackageNotFoundError: 150 | version = "unknown" 151 | 152 | project = "%s/%s " % (project, version) 153 | 154 | self._user_agent = "%skeystonemiddleware.%s/%s" % ( 155 | project, 156 | self.name, 157 | pbr.version.VersionInfo('keystonemiddleware').version_string()) 158 | 159 | return self._user_agent 160 | -------------------------------------------------------------------------------- /keystonemiddleware/oauth2_mtls_token.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 OpenStack Foundation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | import base64 15 | import hashlib 16 | import ssl 17 | import webob 18 | 19 | from oslo_log import log as logging 20 | from oslo_serialization import jsonutils 21 | 22 | from keystonemiddleware.auth_token import _user_plugin 23 | from keystonemiddleware.auth_token import AuthProtocol 24 | from keystonemiddleware import exceptions 25 | from keystonemiddleware.i18n import _ 26 | 27 | 28 | class OAuth2mTlsProtocol(AuthProtocol): 29 | """Middleware that handles OAuth2.0 mutual-TLS client authentication.""" 30 | 31 | def __init__(self, app, conf): 32 | log = logging.getLogger(conf.get('log_name', __name__)) 33 | log.info('Starting Keystone oauth2_mls_token middleware') 34 | super(OAuth2mTlsProtocol, self).__init__(app, conf) 35 | 36 | def _confirm_certificate_thumbprint(self, token_thumb, peer_cert): 37 | """Check if the thumbprint in the token is valid. 38 | 39 | :rtype: if the thumbprint is valid 40 | """ 41 | try: 42 | cert_pem = ssl.DER_cert_to_PEM_cert(peer_cert) 43 | thumb_sha256 = hashlib.sha256(cert_pem.encode('ascii')).digest() 44 | cert_thumb = base64.urlsafe_b64encode(thumb_sha256).decode('ascii') 45 | if cert_thumb == token_thumb: 46 | is_valid = True 47 | else: 48 | self.log.info('The two thumbprints do not match.') 49 | is_valid = False 50 | except Exception as error: 51 | self.log.exception(error) 52 | is_valid = False 53 | return is_valid 54 | 55 | def _is_valid_access_token(self, request): 56 | """Check the OAuth2.0 certificate-bound access token. 57 | 58 | :param request: Incoming request 59 | :rtype: if the access token is valid 60 | """ 61 | try: 62 | wsgi_input = request.environ.get("wsgi.input") 63 | if not wsgi_input: 64 | self.log.warn('Unable to obtain the client certificate.') 65 | return False 66 | sock = wsgi_input.get_socket() 67 | if not sock: 68 | self.log.warn('Unable to obtain the client certificate.') 69 | return False 70 | peer_cert = sock.getpeercert(binary_form=True) 71 | if not peer_cert: 72 | self.log.warn('Unable to obtain the client certificate.') 73 | return False 74 | except Exception as error: 75 | self.log.warn('Unable to obtain the client certificate. %s' % 76 | str(error)) 77 | return False 78 | 79 | access_token = None 80 | if (request.authorization and 81 | request.authorization.authtype == 'Bearer'): 82 | access_token = request.authorization.params 83 | 84 | if not access_token: 85 | self.log.info('Unable to obtain the token.') 86 | return False 87 | 88 | try: 89 | token_data, user_auth_ref = self._do_fetch_token( 90 | access_token, allow_expired=False) 91 | self._validate_token(user_auth_ref, allow_expired=False) 92 | token = token_data.get('token') 93 | oauth2_cred = token.get('oauth2_credential') 94 | if not oauth2_cred: 95 | self.log.info( 96 | 'Invalid OAuth2.0 certificate-bound access token: ' 97 | 'The token is not an OAuth2.0 credential access token.') 98 | return False 99 | 100 | token_thumb = oauth2_cred.get("x5t#S256") 101 | if self._confirm_certificate_thumbprint(token_thumb, peer_cert): 102 | self._confirm_token_bind(user_auth_ref, request) 103 | request.token_info = token_data 104 | request.token_auth = _user_plugin.UserAuthPlugin( 105 | user_auth_ref, None) 106 | return True 107 | else: 108 | self.log.info( 109 | 'Invalid OAuth2.0 certificate-bound access token: ' 110 | 'the access token dose not match the client certificate.') 111 | return False 112 | except exceptions.KeystoneMiddlewareException as err: 113 | self.log.info('Invalid OAuth2.0 certificate-bound access token: %s' 114 | % str(err)) 115 | return False 116 | 117 | def process_request(self, request): 118 | """Process request. 119 | 120 | :param request: Incoming request 121 | :type request: _request.AuthTokenRequest 122 | """ 123 | request.remove_auth_headers() 124 | self._token_cache.initialize(request.environ) 125 | if (not self._is_valid_access_token(request) 126 | or "keystone.token_info" not in request.environ 127 | or "token" not in request.environ["keystone.token_info"]): 128 | self.log.info('Rejecting request') 129 | message = _('The request you have made requires authentication.') 130 | body = {'error': { 131 | 'code': 401, 132 | 'title': 'Unauthorized', 133 | 'message': message, 134 | }} 135 | raise webob.exc.HTTPUnauthorized( 136 | body=jsonutils.dumps(body), 137 | headers=self._reject_auth_headers, 138 | charset='UTF-8', 139 | content_type='application/json') 140 | 141 | request.set_user_headers(request.token_auth.user) 142 | request.set_service_catalog_headers(request.token_auth.user) 143 | request.token_auth._auth = self._auth 144 | request.token_auth._session = self._session 145 | self.log.debug('Accepting request and inited all env fields.') 146 | 147 | 148 | def filter_factory(global_conf, **local_conf): 149 | """Return a WSGI filter app for use with paste.deploy.""" 150 | conf = global_conf.copy() 151 | conf.update(local_conf) 152 | 153 | def auth_filter(app): 154 | return OAuth2mTlsProtocol(app, conf) 155 | 156 | return auth_filter 157 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/auth_token/test_user_auth_plugin.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import uuid 14 | 15 | from keystoneauth1 import fixture 16 | from keystoneauth1 import loading 17 | 18 | from keystonemiddleware.auth_token import _base 19 | from keystonemiddleware.tests.unit.auth_token import base 20 | 21 | # NOTE(jamielennox): just some sample values that we can use for testing 22 | BASE_URI = 'https://keystone.example.com:1234' 23 | AUTH_URL = 'https://keystone.auth.com:1234' 24 | 25 | 26 | class BaseUserPluginTests(object): 27 | 28 | def configure_middleware(self, 29 | auth_type, 30 | **kwargs): 31 | opts = loading.get_auth_plugin_conf_options(auth_type) 32 | self.cfg.register_opts(opts, group=_base.AUTHTOKEN_GROUP) 33 | 34 | # Since these tests cfg.config() themselves rather than waiting for 35 | # auth_token to do it on __init__ we need to register the base auth 36 | # options (e.g., auth_plugin) 37 | loading.register_auth_conf_options(self.cfg.conf, 38 | group=_base.AUTHTOKEN_GROUP) 39 | 40 | self.cfg.config(group=_base.AUTHTOKEN_GROUP, 41 | auth_type=auth_type, 42 | **kwargs) 43 | 44 | def assertTokenDataEqual(self, token_id, token, token_data): 45 | self.assertEqual(token_id, token_data.auth_token) 46 | self.assertEqual(token.user_id, token_data.user_id) 47 | try: 48 | trust_id = token.trust_id 49 | except KeyError: 50 | trust_id = None 51 | self.assertEqual(trust_id, token_data.trust_id) 52 | self.assertEqual(self.get_role_names(token), token_data.role_names) 53 | 54 | def get_plugin(self, token_id, service_token_id=None): 55 | headers = {'X-Auth-Token': token_id} 56 | 57 | if service_token_id: 58 | headers['X-Service-Token'] = service_token_id 59 | 60 | m = self.create_simple_middleware() 61 | 62 | resp = self.call(m, headers=headers) 63 | return resp.request.environ['keystone.token_auth'] 64 | 65 | def test_user_information(self): 66 | token_id, token = self.get_token() 67 | plugin = self.get_plugin(token_id) 68 | 69 | self.assertTokenDataEqual(token_id, token, plugin.user) 70 | self.assertFalse(plugin.has_service_token) 71 | self.assertIsNone(plugin.service) 72 | 73 | def test_with_service_information(self): 74 | token_id, token = self.get_token() 75 | service_id, service = self.get_token(service=True) 76 | 77 | plugin = self.get_plugin(token_id, service_id) 78 | 79 | self.assertTokenDataEqual(token_id, token, plugin.user) 80 | self.assertTokenDataEqual(service_id, service, plugin.service) 81 | 82 | 83 | class V3UserPluginTests(BaseUserPluginTests, base.BaseAuthTokenTestCase): 84 | 85 | def setUp(self): 86 | super(V3UserPluginTests, self).setUp() 87 | 88 | self.service_token_id = uuid.uuid4().hex 89 | self.service_token = fixture.V3Token() 90 | s = self.service_token.add_service('identity', name='keystone') 91 | s.add_standard_endpoints(public=BASE_URI, 92 | admin=BASE_URI, 93 | internal=BASE_URI) 94 | 95 | self.configure_middleware(auth_type='v3password', 96 | auth_url='%s/v3/' % AUTH_URL, 97 | user_id=self.service_token.user_id, 98 | password=uuid.uuid4().hex, 99 | project_id=self.service_token.project_id) 100 | 101 | auth_discovery = fixture.DiscoveryList(href=AUTH_URL) 102 | self.requests_mock.get(AUTH_URL, json=auth_discovery) 103 | 104 | base_discovery = fixture.DiscoveryList(href=BASE_URI) 105 | self.requests_mock.get(BASE_URI, json=base_discovery) 106 | 107 | self.requests_mock.post( 108 | '%s/v3/auth/tokens' % AUTH_URL, 109 | headers={'X-Subject-Token': self.service_token_id}, 110 | json=self.service_token) 111 | 112 | def get_role_names(self, token): 113 | return [x['name'] for x in token['token'].get('roles', [])] 114 | 115 | def get_token(self, project=True, service=False): 116 | token_id = uuid.uuid4().hex 117 | token = fixture.V3Token() 118 | if project: 119 | token.set_project_scope() 120 | token.add_role() 121 | if service: 122 | token.add_role('service') 123 | 124 | request_headers = {'X-Auth-Token': self.service_token_id, 125 | 'X-Subject-Token': token_id} 126 | headers = {'X-Subject-Token': token_id} 127 | 128 | self.requests_mock.get('%s/v3/auth/tokens' % BASE_URI, 129 | request_headers=request_headers, 130 | headers=headers, 131 | json=token) 132 | 133 | return token_id, token 134 | 135 | def assertTokenDataEqual(self, token_id, token, token_data): 136 | super(V3UserPluginTests, self).assertTokenDataEqual(token_id, 137 | token, 138 | token_data) 139 | 140 | self.assertEqual(token.user_domain_id, token_data.user_domain_id) 141 | self.assertEqual(token.project_id, token_data.project_id) 142 | self.assertEqual(token.project_domain_id, token_data.project_domain_id) 143 | 144 | def test_domain_scope(self): 145 | token_id, token = self.get_token(project=False) 146 | token.set_domain_scope() 147 | 148 | plugin = self.get_plugin(token_id) 149 | self.assertEqual(token.domain_id, plugin.user.domain_id) 150 | self.assertIsNone(plugin.user.project_id) 151 | 152 | def test_trust_scope(self): 153 | token_id, token = self.get_token(project=False) 154 | token.set_trust_scope() 155 | 156 | plugin = self.get_plugin(token_id) 157 | self.assertEqual(token.trust_id, plugin.user.trust_id) 158 | self.assertEqual(token.trustor_user_id, plugin.user.trustor_user_id) 159 | self.assertEqual(token.trustee_user_id, plugin.user.trustee_user_id) 160 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/test_ec2_token_middleware.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 OpenStack Foundation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from unittest import mock 16 | 17 | from oslo_serialization import jsonutils 18 | import requests 19 | import webob 20 | 21 | from keystonemiddleware import ec2_token 22 | from keystonemiddleware.tests.unit import utils 23 | 24 | 25 | TOKEN_ID = 'fake-token-id' 26 | EMPTY_RESPONSE = {} 27 | 28 | 29 | class FakeResponse(object): 30 | reason = "Test Reason" 31 | headers = {'x-subject-token': TOKEN_ID} 32 | 33 | def __init__(self, json, status_code=400): 34 | self._json = json 35 | self.status_code = status_code 36 | 37 | def json(self): 38 | return self._json 39 | 40 | 41 | class FakeApp(object): 42 | """This represents a WSGI app protected by the auth_token middleware.""" 43 | 44 | def __call__(self, env, start_response): 45 | resp = webob.Response() 46 | resp.environ = env 47 | return resp(env, start_response) 48 | 49 | 50 | class EC2TokenMiddlewareTestBase(utils.TestCase): 51 | 52 | TEST_PROTOCOL = 'https' 53 | TEST_HOST = 'fakehost' 54 | TEST_PORT = 35357 55 | TEST_URL = '%s://%s:%d/v3/ec2tokens' % (TEST_PROTOCOL, 56 | TEST_HOST, 57 | TEST_PORT) 58 | 59 | def setUp(self): 60 | super(EC2TokenMiddlewareTestBase, self).setUp() 61 | self.middleware = ec2_token.EC2Token(FakeApp(), {}) 62 | 63 | def _validate_ec2_error(self, response, http_status, ec2_code): 64 | self.assertEqual(http_status, response.status_code, 65 | 'Expected HTTP status %s' % http_status) 66 | error_msg = '%s' % ec2_code 67 | error_msg = error_msg.encode() 68 | self.assertIn(error_msg, response.body) 69 | 70 | 71 | class EC2TokenMiddlewareTestGood(EC2TokenMiddlewareTestBase): 72 | @mock.patch.object( 73 | requests, 'post', 74 | return_value=FakeResponse(EMPTY_RESPONSE, status_code=200)) 75 | def test_protocol_old_versions(self, mock_request): 76 | req = webob.Request.blank('/test') 77 | req.GET['Signature'] = 'test-signature' 78 | req.GET['AWSAccessKeyId'] = 'test-key-id' 79 | req.body = b'Action=ListUsers&Version=2010-05-08' 80 | resp = req.get_response(self.middleware) 81 | self.assertEqual(200, resp.status_code) 82 | self.assertEqual(TOKEN_ID, req.headers['X-Auth-Token']) 83 | 84 | mock_request.assert_called_with( 85 | 'http://localhost:5000/v3/ec2tokens', 86 | data=mock.ANY, headers={'Content-Type': 'application/json'}, 87 | verify=True, cert=None, timeout=mock.ANY) 88 | 89 | data = jsonutils.loads(mock_request.call_args[1]['data']) 90 | expected_data = { 91 | 'ec2Credentials': { 92 | 'access': 'test-key-id', 93 | 'headers': {'Host': 'localhost:80', 'Content-Length': '35'}, 94 | 'host': 'localhost:80', 95 | 'verb': 'GET', 96 | 'params': {'AWSAccessKeyId': 'test-key-id'}, 97 | 'signature': 'test-signature', 98 | 'path': '/test', 99 | 'body_hash': 'b6359072c78d70ebee1e81adcbab4f01' 100 | 'bf2c23245fa365ef83fe8f1f955085e2'}} 101 | self.assertDictEqual(expected_data, data) 102 | 103 | @mock.patch.object( 104 | requests, 'post', 105 | return_value=FakeResponse(EMPTY_RESPONSE, status_code=200)) 106 | def test_protocol_v4(self, mock_request): 107 | req = webob.Request.blank('/test') 108 | auth_str = ( 109 | 'AWS4-HMAC-SHA256' 110 | ' Credential=test-key-id/20110909/us-east-1/iam/aws4_request,' 111 | ' SignedHeaders=content-type;host;x-amz-date,' 112 | ' Signature=test-signature') 113 | req.headers['Authorization'] = auth_str 114 | req.body = b'Action=ListUsers&Version=2010-05-08' 115 | resp = req.get_response(self.middleware) 116 | self.assertEqual(200, resp.status_code) 117 | self.assertEqual(TOKEN_ID, req.headers['X-Auth-Token']) 118 | 119 | mock_request.assert_called_with( 120 | 'http://localhost:5000/v3/ec2tokens', 121 | data=mock.ANY, headers={'Content-Type': 'application/json'}, 122 | verify=True, cert=None, timeout=mock.ANY) 123 | 124 | data = jsonutils.loads(mock_request.call_args[1]['data']) 125 | expected_data = { 126 | 'ec2Credentials': { 127 | 'access': 'test-key-id', 128 | 'headers': {'Host': 'localhost:80', 129 | 'Content-Length': '35', 130 | 'Authorization': auth_str}, 131 | 'host': 'localhost:80', 132 | 'verb': 'GET', 133 | 'params': {}, 134 | 'signature': 'test-signature', 135 | 'path': '/test', 136 | 'body_hash': 'b6359072c78d70ebee1e81adcbab4f01' 137 | 'bf2c23245fa365ef83fe8f1f955085e2'}} 138 | self.assertDictEqual(expected_data, data) 139 | 140 | 141 | class EC2TokenMiddlewareTestBad(EC2TokenMiddlewareTestBase): 142 | 143 | def test_no_signature(self): 144 | req = webob.Request.blank('/test') 145 | resp = req.get_response(self.middleware) 146 | self._validate_ec2_error(resp, 400, 'AuthFailure') 147 | 148 | def test_no_key_id(self): 149 | req = webob.Request.blank('/test') 150 | req.GET['Signature'] = 'test-signature' 151 | resp = req.get_response(self.middleware) 152 | self._validate_ec2_error(resp, 400, 'AuthFailure') 153 | 154 | @mock.patch.object( 155 | requests, 'post', 156 | return_value=FakeResponse(EMPTY_RESPONSE)) 157 | def test_communication_failure(self, mock_request): 158 | req = webob.Request.blank('/test') 159 | req.GET['Signature'] = 'test-signature' 160 | req.GET['AWSAccessKeyId'] = 'test-key-id' 161 | resp = req.get_response(self.middleware) 162 | self._validate_ec2_error(resp, 400, 'AuthFailure') 163 | mock_request.assert_called_with( 164 | 'http://localhost:5000/v3/ec2tokens', 165 | data=mock.ANY, headers=mock.ANY, 166 | verify=mock.ANY, cert=mock.ANY, timeout=mock.ANY) 167 | 168 | @mock.patch.object( 169 | requests, 'post', 170 | return_value=FakeResponse(EMPTY_RESPONSE)) 171 | def test_no_result_data(self, mock_request): 172 | req = webob.Request.blank('/test') 173 | req.GET['Signature'] = 'test-signature' 174 | req.GET['AWSAccessKeyId'] = 'test-key-id' 175 | resp = req.get_response(self.middleware) 176 | self._validate_ec2_error(resp, 400, 'AuthFailure') 177 | mock_request.assert_called_with( 178 | 'http://localhost:5000/v3/ec2tokens', 179 | data=mock.ANY, headers=mock.ANY, 180 | verify=mock.ANY, cert=mock.ANY, timeout=mock.ANY) 181 | -------------------------------------------------------------------------------- /keystonemiddleware/auth_token/_memcache_crypt.py: -------------------------------------------------------------------------------- 1 | # Copyright 2010-2013 OpenStack Foundation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | Utilities for memcache encryption and integrity check. 18 | 19 | Data should be serialized before entering these functions. Encryption 20 | has a dependency on the cryptography module. If cryptography is not 21 | available, CryptoUnavailableError will be raised. 22 | 23 | This module will not be called unless signing or encryption is enabled 24 | in the config. It will always validate signatures, and will decrypt 25 | data if encryption is enabled. It is not valid to mix protection 26 | modes. 27 | 28 | """ 29 | 30 | import base64 31 | import functools 32 | import hashlib 33 | import hmac 34 | import math 35 | import os 36 | 37 | from keystonemiddleware.i18n import _ 38 | 39 | try: 40 | from cryptography.hazmat import backends as crypto_backends 41 | from cryptography.hazmat.primitives import ciphers 42 | from cryptography.hazmat.primitives.ciphers import algorithms 43 | from cryptography.hazmat.primitives.ciphers import modes 44 | from cryptography.hazmat.primitives import padding 45 | except ImportError: 46 | ciphers = None 47 | 48 | 49 | HASH_FUNCTION = hashlib.sha384 50 | DIGEST_LENGTH = HASH_FUNCTION().digest_size 51 | DIGEST_SPLIT = DIGEST_LENGTH // 3 52 | DIGEST_LENGTH_B64 = 4 * int(math.ceil(DIGEST_LENGTH / 3.0)) 53 | 54 | 55 | class InvalidMacError(Exception): 56 | """raise when unable to verify MACed data. 57 | 58 | This usually indicates that data had been expectedly modified in memcache. 59 | 60 | """ 61 | 62 | pass 63 | 64 | 65 | class DecryptError(Exception): 66 | """raise when unable to decrypt encrypted data.""" 67 | 68 | pass 69 | 70 | 71 | class CryptoUnavailableError(Exception): 72 | """raise when Python Crypto module is not available.""" 73 | 74 | pass 75 | 76 | 77 | def assert_crypto_availability(f): 78 | """Ensure cryptography module is available.""" 79 | @functools.wraps(f) 80 | def wrapper(*args, **kwds): 81 | if ciphers is None: 82 | raise CryptoUnavailableError() 83 | return f(*args, **kwds) 84 | return wrapper 85 | 86 | 87 | def derive_keys(token, secret, strategy): 88 | """Derive keys for MAC and ENCRYPTION from the user-provided secret. 89 | 90 | The resulting keys should be passed to the protect and unprotect functions. 91 | 92 | As suggested by NIST Special Publication 800-108, this uses the 93 | first 128 bits from the sha384 KDF for the obscured cache key 94 | value, the second 128 bits for the message authentication key and 95 | the remaining 128 bits for the encryption key. 96 | 97 | This approach is faster than computing a separate hmac as the KDF 98 | for each desired key. 99 | """ 100 | if not isinstance(secret, bytes): 101 | secret = secret.encode() 102 | 103 | if not isinstance(token, bytes): 104 | token = token.encode() 105 | 106 | if not isinstance(strategy, bytes): 107 | strategy = strategy.encode() 108 | 109 | digest = hmac.new(secret, token + strategy, HASH_FUNCTION).digest() 110 | return {'CACHE_KEY': digest[:DIGEST_SPLIT], 111 | 'MAC': digest[DIGEST_SPLIT: 2 * DIGEST_SPLIT], 112 | 'ENCRYPTION': digest[2 * DIGEST_SPLIT:], 113 | 'strategy': strategy} 114 | 115 | 116 | def sign_data(key, data): 117 | """Sign the data using the defined function and the derived key.""" 118 | if not isinstance(key, bytes): 119 | key = key.encode() 120 | 121 | if not isinstance(data, bytes): 122 | data = data.encode() 123 | 124 | mac = hmac.new(key, data, HASH_FUNCTION).digest() 125 | return base64.b64encode(mac) 126 | 127 | 128 | @assert_crypto_availability 129 | def encrypt_data(key, data): 130 | """Encrypt the data with the given secret key. 131 | 132 | Padding is n bytes of the value n, where 1 <= n <= blocksize. 133 | """ 134 | iv = os.urandom(16) 135 | cipher = ciphers.Cipher( 136 | algorithms.AES(key), 137 | modes.CBC(iv), 138 | backend=crypto_backends.default_backend()) 139 | 140 | # AES algorithm uses block size of 16 bytes = 128 bits, defined in 141 | # algorithms.AES.block_size. Previously, we manually padded this using 142 | # bytes((padding,)) * padding. Using ``cryptography``, we will 143 | # analogously use hazmat.primitives.padding to pad it to 144 | # the 128-bit block size. 145 | padder = padding.PKCS7(algorithms.AES.block_size).padder() 146 | padded_data = padder.update(data) + padder.finalize() 147 | encryptor = cipher.encryptor() 148 | return iv + encryptor.update(padded_data) + encryptor.finalize() 149 | 150 | 151 | def decrypt_data(key, data): 152 | """Decrypt the data with the given secret key.""" 153 | iv = data[:16] 154 | cipher = ciphers.Cipher( 155 | algorithms.AES(key), 156 | modes.CBC(iv), 157 | backend=crypto_backends.default_backend()) 158 | try: 159 | decryptor = cipher.decryptor() 160 | result = decryptor.update(data[16:]) + decryptor.finalize() 161 | except Exception: 162 | raise DecryptError(_('Encrypted data appears to be corrupted.')) 163 | 164 | # Strip the last n padding bytes where n is the last value in 165 | # the plaintext 166 | unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() 167 | return unpadder.update(result) + unpadder.finalize() 168 | 169 | 170 | def protect_data(keys, data): 171 | """Serialize data given a dict of keys. 172 | 173 | Given keys and serialized data, returns an appropriately protected string 174 | suitable for storage in the cache. 175 | 176 | """ 177 | if keys['strategy'] == b'ENCRYPT': 178 | data = encrypt_data(keys['ENCRYPTION'], data) 179 | 180 | encoded_data = base64.b64encode(data) 181 | 182 | signature = sign_data(keys['MAC'], encoded_data) 183 | return signature + encoded_data 184 | 185 | 186 | def unprotect_data(keys, signed_data): 187 | """De-serialize data given a dict of keys. 188 | 189 | Given keys and cached string data, verifies the signature, decrypts if 190 | necessary, and returns the original serialized data. 191 | 192 | """ 193 | # cache backends return None when no data is found. We don't mind 194 | # that this particular special value is unsigned. 195 | if signed_data is None: 196 | return None 197 | 198 | # First we calculate the signature 199 | provided_mac = signed_data[:DIGEST_LENGTH_B64] 200 | calculated_mac = sign_data( 201 | keys['MAC'], 202 | signed_data[DIGEST_LENGTH_B64:]) 203 | 204 | # Then verify that it matches the provided value 205 | if not hmac.compare_digest(provided_mac, calculated_mac): 206 | raise InvalidMacError(_('Invalid MAC; data appears to be corrupted.')) 207 | 208 | data = base64.b64decode(signed_data[DIGEST_LENGTH_B64:]) 209 | 210 | # then if necessary decrypt the data 211 | if keys['strategy'] == b'ENCRYPT': 212 | data = decrypt_data(keys['ENCRYPTION'], data) 213 | 214 | return data 215 | 216 | 217 | def get_cache_key(keys): 218 | """Return a cache key. 219 | 220 | Given keys generated by derive_keys(), returns a base64 encoded value 221 | suitable for use as a cache key in memcached. 222 | 223 | """ 224 | return base64.b64encode(keys['CACHE_KEY']) 225 | -------------------------------------------------------------------------------- /keystonemiddleware/auth_token/_identity.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import urllib.parse 14 | 15 | from keystoneauth1 import discover 16 | from keystoneauth1 import exceptions as ksa_exceptions 17 | from keystoneauth1 import plugin 18 | from keystoneclient.v3 import client as v3_client 19 | 20 | from keystonemiddleware.auth_token import _auth 21 | from keystonemiddleware.auth_token import _exceptions as ksm_exceptions 22 | from keystonemiddleware.i18n import _ 23 | 24 | ACCESS_RULES_SUPPORT = '1' 25 | 26 | 27 | class _RequestStrategy(object): 28 | 29 | AUTH_VERSION = None 30 | 31 | def __init__(self, adap, include_service_catalog=None, 32 | requested_auth_interface=None): 33 | self._include_service_catalog = include_service_catalog 34 | self._requested_auth_interface = requested_auth_interface 35 | 36 | def verify_token(self, user_token, allow_expired=False): 37 | pass 38 | 39 | 40 | class _V3RequestStrategy(_RequestStrategy): 41 | 42 | AUTH_VERSION = (3, 0) 43 | 44 | def __init__(self, adap, **kwargs): 45 | super(_V3RequestStrategy, self).__init__(adap, **kwargs) 46 | client_args = {'session': adap} 47 | if self._requested_auth_interface: 48 | client_args['interface'] = self._requested_auth_interface 49 | self._client = v3_client.Client(**client_args) 50 | 51 | def verify_token(self, token, allow_expired=False): 52 | auth_ref = self._client.tokens.validate( 53 | token, 54 | include_catalog=self._include_service_catalog, 55 | allow_expired=allow_expired, 56 | access_rules_support=ACCESS_RULES_SUPPORT) 57 | 58 | if not auth_ref: 59 | msg = _('Failed to fetch token data from identity server') 60 | raise ksm_exceptions.InvalidToken(msg) 61 | 62 | return {'token': auth_ref} 63 | 64 | 65 | _REQUEST_STRATEGIES = [_V3RequestStrategy] 66 | 67 | 68 | class IdentityServer(object): 69 | """Base class for operations on the Identity API server. 70 | 71 | The auth_token middleware needs to communicate with the Identity API server 72 | to validate tokens. This class encapsulates the data and methods to perform 73 | the operations. 74 | 75 | """ 76 | 77 | def __init__(self, log, adap, include_service_catalog=None, 78 | requested_auth_version=None, requested_auth_interface=None): 79 | self._LOG = log 80 | self._adapter = adap 81 | self._include_service_catalog = include_service_catalog 82 | self._requested_auth_version = requested_auth_version 83 | self._requested_auth_interface = requested_auth_interface 84 | 85 | # Built on-demand with self._request_strategy. 86 | self._request_strategy_obj = None 87 | 88 | @property 89 | def www_authenticate_uri(self): 90 | www_authenticate_uri = self._adapter.get_endpoint( 91 | interface=plugin.AUTH_INTERFACE) 92 | 93 | # NOTE(jamielennox): This weird stripping of the prefix hack is 94 | # only relevant to the legacy case. We urljoin '/' to get just the 95 | # base URI as this is the original behaviour. 96 | if isinstance(self._adapter.auth, _auth.AuthTokenPlugin): 97 | www_authenticate_uri = urllib.parse.urljoin( 98 | www_authenticate_uri, '/').rstrip('/') 99 | 100 | return www_authenticate_uri 101 | 102 | @property 103 | def auth_version(self): 104 | return self._request_strategy.AUTH_VERSION 105 | 106 | @property 107 | def _request_strategy(self): 108 | if not self._request_strategy_obj: 109 | strategy_class = self._get_strategy_class() 110 | self._adapter.version = strategy_class.AUTH_VERSION 111 | 112 | self._request_strategy_obj = strategy_class( 113 | self._adapter, 114 | include_service_catalog=self._include_service_catalog, 115 | requested_auth_interface=self._requested_auth_interface) 116 | 117 | return self._request_strategy_obj 118 | 119 | def _get_strategy_class(self): 120 | if self._requested_auth_version: 121 | if not discover.version_match(_V3RequestStrategy.AUTH_VERSION, 122 | self._requested_auth_interface): 123 | self._LOG.info('A version other than v3 was requested: %s', 124 | self._requested_auth_interface) 125 | # Return v3, even if the request is unknown 126 | return _V3RequestStrategy 127 | 128 | # Specific version was not requested then we fall through to 129 | # discovering available versions from the server 130 | for klass in _REQUEST_STRATEGIES: 131 | if self._adapter.get_endpoint(version=klass.AUTH_VERSION): 132 | self._LOG.debug('Auth Token confirmed use of %s apis', 133 | klass.AUTH_VERSION) 134 | return klass 135 | 136 | versions = ['v%d.%d' % s.AUTH_VERSION for s in _REQUEST_STRATEGIES] 137 | self._LOG.error('No attempted versions [%s] supported by server', 138 | ', '.join(versions)) 139 | 140 | msg = _('No compatible apis supported by server') 141 | raise ksm_exceptions.ServiceError(msg) 142 | 143 | def verify_token(self, user_token, retry=True, allow_expired=False): 144 | """Authenticate user token with identity server. 145 | 146 | :param user_token: user's token id 147 | :param retry: flag that forces the middleware to retry 148 | user authentication when an indeterminate 149 | response is received. Optional. 150 | :param allow_expired: Allow retrieving an expired token. 151 | :returns: access info received from identity server on success 152 | :rtype: :py:class:`keystoneauth1.access.AccessInfo` 153 | :raises exc.InvalidToken: if token is rejected 154 | :raises exc.ServiceError: if unable to authenticate token 155 | 156 | """ 157 | try: 158 | auth_ref = self._request_strategy.verify_token( 159 | user_token, 160 | allow_expired=allow_expired) 161 | except ksa_exceptions.NotFound as e: 162 | self._LOG.info('Authorization failed for token') 163 | self._LOG.info('Identity response: %s', e.response.text) 164 | raise ksm_exceptions.InvalidToken(_('Token authorization failed')) 165 | except ksa_exceptions.Unauthorized as e: 166 | self._LOG.info('Identity server rejected authorization') 167 | self._LOG.warning('Identity response: %s', e.response.text) 168 | if retry: 169 | self._LOG.info('Retrying validation') 170 | return self.verify_token(user_token, False) 171 | msg = _('Identity server rejected authorization necessary to ' 172 | 'fetch token data') 173 | raise ksm_exceptions.ServiceError(msg) 174 | except ksa_exceptions.HttpError as e: 175 | self._LOG.error( 176 | 'Bad response code while validating token: %s %s', 177 | e.http_status, e.message) 178 | if hasattr(e.response, 'text'): 179 | self._LOG.warning('Identity response: %s', e.response.text) 180 | msg = _('Failed to fetch token data from identity server') 181 | raise ksm_exceptions.ServiceError(msg) 182 | else: 183 | return auth_ref 184 | 185 | def invalidate(self): 186 | return self._adapter.invalidate() 187 | -------------------------------------------------------------------------------- /keystonemiddleware/tests/unit/auth_token/test_cache.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import uuid 14 | 15 | import fixtures 16 | from unittest import mock 17 | 18 | from keystonemiddleware.auth_token import _cache 19 | from keystonemiddleware.auth_token import _exceptions as exc 20 | from keystonemiddleware.tests.unit.auth_token import base 21 | from keystonemiddleware.tests.unit import utils 22 | 23 | MEMCACHED_SERVERS = ['localhost:11211'] 24 | MEMCACHED_AVAILABLE = None 25 | 26 | 27 | class TestCacheSetup(base.BaseAuthTokenTestCase): 28 | 29 | def test_assert_valid_memcache_protection_config(self): 30 | # test missing memcache_secret_key 31 | conf = { 32 | 'memcached_servers': ','.join(MEMCACHED_SERVERS), 33 | 'memcache_security_strategy': 'Encrypt' 34 | } 35 | self.assertRaises(exc.ConfigurationError, 36 | self.create_simple_middleware, 37 | conf=conf) 38 | # test invalue memcache_security_strategy 39 | conf = { 40 | 'memcached_servers': ','.join(MEMCACHED_SERVERS), 41 | 'memcache_security_strategy': 'whatever' 42 | } 43 | self.assertRaises(exc.ConfigurationError, 44 | self.create_simple_middleware, 45 | conf=conf) 46 | # test missing memcache_secret_key 47 | conf = { 48 | 'memcached_servers': ','.join(MEMCACHED_SERVERS), 49 | 'memcache_security_strategy': 'mac' 50 | } 51 | self.assertRaises(exc.ConfigurationError, 52 | self.create_simple_middleware, 53 | conf=conf) 54 | conf = { 55 | 'memcached_servers': ','.join(MEMCACHED_SERVERS), 56 | 'memcache_security_strategy': 'Encrypt', 57 | 'memcache_secret_key': '' 58 | } 59 | self.assertRaises(exc.ConfigurationError, 60 | self.create_simple_middleware, 61 | conf=conf) 62 | conf = { 63 | 'memcached_servers': ','.join(MEMCACHED_SERVERS), 64 | 'memcache_security_strategy': 'mAc', 65 | 'memcache_secret_key': '' 66 | } 67 | self.assertRaises(exc.ConfigurationError, 68 | self.create_simple_middleware, 69 | conf=conf) 70 | 71 | 72 | class NoMemcacheAuthToken(base.BaseAuthTokenTestCase): 73 | """These tests will not have the memcache module available.""" 74 | 75 | def setUp(self): 76 | super(NoMemcacheAuthToken, self).setUp() 77 | self.useFixture(utils.DisableModuleFixture('memcache')) 78 | 79 | def test_nomemcache(self): 80 | conf = { 81 | 'admin_token': 'admin_token1', 82 | 'auth_host': 'keystone.example.com', 83 | 'auth_port': '1234', 84 | 'memcached_servers': ','.join(MEMCACHED_SERVERS), 85 | 'www_authenticate_uri': 'https://keystone.example.com:1234', 86 | } 87 | 88 | self.create_simple_middleware(conf=conf) 89 | 90 | 91 | class TestLiveMemcache(base.BaseAuthTokenTestCase): 92 | 93 | def setUp(self): 94 | super(TestLiveMemcache, self).setUp() 95 | 96 | global MEMCACHED_AVAILABLE 97 | 98 | if MEMCACHED_AVAILABLE is None: 99 | try: 100 | import memcache 101 | c = memcache.Client(MEMCACHED_SERVERS) 102 | c.set('ping', 'pong', time=1) 103 | MEMCACHED_AVAILABLE = c.get('ping') == 'pong' 104 | except ImportError: 105 | MEMCACHED_AVAILABLE = False 106 | 107 | if not MEMCACHED_AVAILABLE: 108 | self.skipTest('memcached not available') 109 | 110 | def test_encrypt_cache_data(self): 111 | conf = { 112 | 'memcached_servers': ','.join(MEMCACHED_SERVERS), 113 | 'memcache_security_strategy': 'encrypt', 114 | 'memcache_secret_key': 'mysecret' 115 | } 116 | 117 | token = uuid.uuid4().hex.encode() 118 | data = uuid.uuid4().hex 119 | 120 | token_cache = self.create_simple_middleware(conf=conf)._token_cache 121 | token_cache.initialize({}) 122 | 123 | token_cache.set(token, data) 124 | self.assertEqual(token_cache.get(token), data) 125 | 126 | @mock.patch("keystonemiddleware.auth_token._memcache_crypt.unprotect_data") 127 | def test_corrupted_cache_data(self, mocked_decrypt_data): 128 | mocked_decrypt_data.side_effect = Exception("corrupted") 129 | 130 | conf = { 131 | 'memcached_servers': ','.join(MEMCACHED_SERVERS), 132 | 'memcache_security_strategy': 'encrypt', 133 | 'memcache_secret_key': 'mysecret' 134 | } 135 | 136 | token = uuid.uuid4().hex.encode() 137 | data = uuid.uuid4().hex 138 | 139 | token_cache = self.create_simple_middleware(conf=conf)._token_cache 140 | token_cache.initialize({}) 141 | 142 | token_cache.set(token, data) 143 | self.assertIsNone(token_cache.get(token)) 144 | 145 | def test_sign_cache_data(self): 146 | conf = { 147 | 'memcached_servers': ','.join(MEMCACHED_SERVERS), 148 | 'memcache_security_strategy': 'mac', 149 | 'memcache_secret_key': 'mysecret' 150 | } 151 | 152 | token = uuid.uuid4().hex.encode() 153 | data = uuid.uuid4().hex 154 | 155 | token_cache = self.create_simple_middleware(conf=conf)._token_cache 156 | token_cache.initialize({}) 157 | 158 | token_cache.set(token, data) 159 | self.assertEqual(token_cache.get(token), data) 160 | 161 | def test_no_memcache_protection(self): 162 | conf = { 163 | 'memcached_servers': ','.join(MEMCACHED_SERVERS), 164 | 'memcache_secret_key': 'mysecret' 165 | } 166 | 167 | token = uuid.uuid4().hex.encode() 168 | data = uuid.uuid4().hex 169 | 170 | token_cache = self.create_simple_middleware(conf=conf)._token_cache 171 | token_cache.initialize({}) 172 | token_cache.set(token, data) 173 | self.assertEqual(token_cache.get(token), data) 174 | 175 | def test_memcache_pool(self): 176 | conf = { 177 | 'memcached_servers': ','.join(MEMCACHED_SERVERS), 178 | 'memcache_use_advanced_pool': True 179 | } 180 | 181 | token = uuid.uuid4().hex.encode() 182 | data = uuid.uuid4().hex 183 | 184 | token_cache = self.create_simple_middleware(conf=conf)._token_cache 185 | token_cache.initialize({}) 186 | 187 | token_cache.set(token, data) 188 | self.assertEqual(token_cache.get(token), data) 189 | 190 | 191 | class TestMemcachePoolAbstraction(utils.TestCase): 192 | def setUp(self): 193 | super(TestMemcachePoolAbstraction, self).setUp() 194 | self.useFixture(fixtures.MockPatch( 195 | 'oslo_cache._memcache_pool._MemcacheClient')) 196 | 197 | def test_abstraction_layer_reserve_places_connection_back_in_pool(self): 198 | cache_pool = _cache._MemcacheClientPool( 199 | memcache_servers=[], arguments={}, maxsize=1, unused_timeout=10) 200 | conn = None 201 | with cache_pool.reserve() as client: 202 | self.assertEqual(cache_pool._pool._acquired, 1) 203 | conn = client 204 | 205 | self.assertEqual(cache_pool._pool._acquired, 0) 206 | with cache_pool.reserve() as client: 207 | # Make sure the connection we got before is in-fact the one we 208 | # get again. 209 | self.assertEqual(conn, client) 210 | -------------------------------------------------------------------------------- /keystonemiddleware/audit/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | 14 | """ 15 | Build open standard audit information based on incoming requests. 16 | 17 | AuditMiddleware filter should be placed after keystonemiddleware.auth_token 18 | in the pipeline so that it can utilise the information the Identity server 19 | provides. 20 | """ 21 | 22 | import copy 23 | import functools 24 | 25 | from oslo_config import cfg 26 | from oslo_context import context as oslo_context 27 | from oslo_log import log as logging 28 | from pycadf import cadftaxonomy as taxonomy 29 | from pycadf import cadftype 30 | from pycadf import reason 31 | from pycadf import reporterstep 32 | from pycadf import resource 33 | from pycadf import timestamp 34 | import webob.dec 35 | 36 | from keystonemiddleware._common import config 37 | from keystonemiddleware.audit import _api 38 | from keystonemiddleware.audit import _notifier 39 | 40 | 41 | _LOG = None 42 | AUDIT_MIDDLEWARE_GROUP = 'audit_middleware_notifications' 43 | 44 | _AUDIT_OPTS = [ 45 | cfg.BoolOpt('use_oslo_messaging', 46 | default=True, 47 | help='Indicate whether to use oslo_messaging as the notifier. ' 48 | 'If set to False, the local logger will be used as the ' 49 | 'notifier. If set to True, the oslo_messaging package ' 50 | 'must also be present. Otherwise, the local will be used ' 51 | 'instead.'), 52 | cfg.StrOpt('driver', 53 | help='The Driver to handle sending notifications. Possible ' 54 | 'values are messaging, messagingv2, routing, log, test, ' 55 | 'noop. If not specified, then value from ' 56 | 'oslo_messaging_notifications conf section is used.'), 57 | cfg.ListOpt('topics', 58 | help='List of AMQP topics used for OpenStack notifications. If' 59 | ' not specified, then value from ' 60 | ' oslo_messaging_notifications conf section is used.'), 61 | cfg.StrOpt('transport_url', 62 | secret=True, 63 | help='A URL representing messaging driver to use for ' 64 | 'notification. If not specified, we fall back to the same ' 65 | 'configuration used for RPC.'), 66 | ] 67 | CONF = cfg.CONF 68 | CONF.register_opts(_AUDIT_OPTS, group=AUDIT_MIDDLEWARE_GROUP) 69 | 70 | 71 | def _log_and_ignore_error(fn): 72 | @functools.wraps(fn) 73 | def wrapper(*args, **kwargs): 74 | try: 75 | return fn(*args, **kwargs) 76 | except Exception as e: 77 | _LOG.exception('An exception occurred processing ' 78 | 'the API call: %s ', e) 79 | return wrapper 80 | 81 | 82 | class AuditMiddleware(object): 83 | """Create an audit event based on request/response. 84 | 85 | The audit middleware takes in various configuration options such as the 86 | ability to skip audit of certain requests. The full list of options can 87 | be discovered here: 88 | https://docs.openstack.org/keystonemiddleware/latest/audit.html 89 | """ 90 | 91 | def __init__(self, app, **conf): 92 | self._application = app 93 | self._conf = config.Config('audit', 94 | AUDIT_MIDDLEWARE_GROUP, 95 | list_opts(), 96 | conf) 97 | global _LOG 98 | _LOG = logging.getLogger(conf.get('log_name', __name__)) 99 | self._service_name = conf.get('service_name') 100 | self._ignore_req_list = [x.upper().strip() for x in 101 | conf.get('ignore_req_list', '').split(',')] 102 | self._cadf_audit = _api.OpenStackAuditApi(conf.get('audit_map_file'), 103 | _LOG) 104 | self._notifier = _notifier.create_notifier(self._conf, _LOG) 105 | 106 | def _create_event(self, req): 107 | event = self._cadf_audit._create_event(req) 108 | # cache model in request to allow tracking of transistive steps. 109 | req.environ['cadf_event'] = event 110 | return event 111 | 112 | @_log_and_ignore_error 113 | def _process_request(self, request): 114 | self._notifier.notify(request.environ['audit.context'], 115 | 'audit.http.request', 116 | self._create_event(request).as_dict()) 117 | 118 | @_log_and_ignore_error 119 | def _process_response(self, request, response=None): 120 | # NOTE(gordc): handle case where error processing request 121 | if 'cadf_event' not in request.environ: 122 | self._create_event(request) 123 | event = request.environ['cadf_event'] 124 | 125 | if response: 126 | if response.status_int >= 200 and response.status_int < 400: 127 | result = taxonomy.OUTCOME_SUCCESS 128 | else: 129 | result = taxonomy.OUTCOME_FAILURE 130 | event.reason = reason.Reason( 131 | reasonType='HTTP', reasonCode=str(response.status_int)) 132 | else: 133 | result = taxonomy.UNKNOWN 134 | 135 | event.outcome = result 136 | event.add_reporterstep( 137 | reporterstep.Reporterstep( 138 | role=cadftype.REPORTER_ROLE_MODIFIER, 139 | reporter=resource.Resource(id='target'), 140 | reporterTime=timestamp.get_utc_now())) 141 | 142 | self._notifier.notify(request.environ['audit.context'], 143 | 'audit.http.response', 144 | event.as_dict()) 145 | 146 | @webob.dec.wsgify 147 | def __call__(self, req): 148 | if req.method in self._ignore_req_list: 149 | return req.get_response(self._application) 150 | 151 | # Cannot use a RequestClass on wsgify above because the `req` object is 152 | # a `WebOb.Request` when this method is called so the RequestClass is 153 | # ignored by the wsgify wrapper. 154 | req.environ['audit.context'] = \ 155 | oslo_context.get_admin_context().to_dict() 156 | 157 | self._process_request(req) 158 | try: 159 | response = req.get_response(self._application) 160 | except Exception: 161 | self._process_response(req) 162 | raise 163 | else: 164 | self._process_response(req, response) 165 | return response 166 | 167 | 168 | def list_opts(): 169 | """Return a list of oslo_config options available in audit middleware. 170 | 171 | The returned list includes all oslo_config options which may be registered 172 | at runtime by the project. 173 | 174 | Each element of the list is a tuple. The first element is the name of the 175 | group under which the list of elements in the second element will be 176 | registered. A group name of None corresponds to the [DEFAULT] group in 177 | config files. 178 | 179 | :returns: a list of (group_name, opts) tuples 180 | """ 181 | return [(AUDIT_MIDDLEWARE_GROUP, copy.deepcopy(_AUDIT_OPTS))] 182 | 183 | 184 | def filter_factory(global_conf, **local_conf): 185 | """Return a WSGI filter app for use with paste.deploy.""" 186 | conf = global_conf.copy() 187 | conf.update(local_conf) 188 | 189 | def audit_filter(app): 190 | return AuditMiddleware(app, **conf) 191 | return audit_filter 192 | 193 | 194 | # NOTE(jamielennox): Maintained here for public API compatibility. 195 | Service = _api.Service 196 | AuditMap = _api.AuditMap 197 | PycadfAuditApiConfigError = _api.PycadfAuditApiConfigError 198 | OpenStackAuditApi = _api.OpenStackAuditApi 199 | ClientResource = _api.ClientResource 200 | KeystoneCredential = _api.KeystoneCredential 201 | -------------------------------------------------------------------------------- /keystonemiddleware/auth_token/_auth.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | from keystoneauth1 import discover 14 | from keystoneauth1.identity import v2 15 | from keystoneauth1 import plugin 16 | from keystoneauth1 import token_endpoint 17 | from oslo_config import cfg 18 | from oslo_utils import netutils 19 | 20 | from keystonemiddleware.auth_token import _base 21 | from keystonemiddleware.i18n import _ 22 | 23 | 24 | class AuthTokenPlugin(plugin.BaseAuthPlugin): 25 | 26 | def __init__(self, auth_host, auth_port, auth_protocol, auth_admin_prefix, 27 | admin_user, admin_password, admin_tenant_name, admin_token, 28 | identity_uri, log): 29 | 30 | log.warning( 31 | "Use of the auth_admin_prefix, auth_host, auth_port, " 32 | "auth_protocol, identity_uri, admin_token, admin_user, " 33 | "admin_password, and admin_tenant_name configuration options was " 34 | "deprecated in the Mitaka release in favor of an auth_plugin and " 35 | "its related options. This class may be removed in a future " 36 | "release.") 37 | 38 | # NOTE(jamielennox): it does appear here that our default arguments 39 | # are backwards. We need to do it this way so that we can handle the 40 | # same deprecation strategy for CONF and the conf variable. 41 | if not identity_uri: 42 | log.warning('Configuring admin URI using auth fragments was ' 43 | 'deprecated in the Kilo release, and will be ' 44 | 'removed in the Newton release, ' 45 | "use 'identity_uri' instead.") 46 | 47 | identity_uri = '%s://%s:%s' % (auth_protocol, 48 | netutils.escape_ipv6(auth_host), 49 | auth_port) 50 | 51 | if auth_admin_prefix: 52 | identity_uri = '%s/%s' % (identity_uri, 53 | auth_admin_prefix.strip('/')) 54 | 55 | self._identity_uri = identity_uri.rstrip('/') 56 | 57 | # FIXME(jamielennox): Yes. This is wrong. We should be determining the 58 | # plugin to use based on a combination of discovery and inputs. Much 59 | # of this can be changed when we get keystoneclient 0.10. For now this 60 | # hardcoded path is EXACTLY the same as the original auth_token did. 61 | auth_url = '%s/v2.0' % self._identity_uri 62 | 63 | if admin_token: 64 | log.warning( 65 | "The admin_token option in auth_token middleware was " 66 | "deprecated in the Kilo release, and will be removed in the " 67 | "Newton release, use admin_user and admin_password instead.") 68 | self._plugin = token_endpoint.Token(auth_url, admin_token) 69 | else: 70 | self._plugin = v2.Password(auth_url, 71 | username=admin_user, 72 | password=admin_password, 73 | tenant_name=admin_tenant_name) 74 | 75 | self._LOG = log 76 | self._discover = None 77 | 78 | def get_token(self, *args, **kwargs): 79 | return self._plugin.get_token(*args, **kwargs) 80 | 81 | def get_endpoint(self, session, interface=None, version=None, **kwargs): 82 | """Return an endpoint for the client. 83 | 84 | There are no required keyword arguments to ``get_endpoint`` as a plugin 85 | implementation should use best effort with the information available to 86 | determine the endpoint. 87 | 88 | :param session: The session object that the auth_plugin belongs to. 89 | :type session: keystoneauth1.session.Session 90 | :param version: The version number required for this endpoint. 91 | :type version: tuple or str 92 | :param str interface: what visibility the endpoint should have. 93 | 94 | :returns: The base URL that will be used to talk to the required 95 | service or None if not available. 96 | :rtype: string 97 | """ 98 | if interface == plugin.AUTH_INTERFACE: 99 | return self._identity_uri 100 | 101 | if not version: 102 | # NOTE(jamielennox): This plugin can only be used within auth_token 103 | # and auth_token will always provide version= with requests. 104 | return None 105 | 106 | if not self._discover: 107 | self._discover = discover.Discover(session, 108 | url=self._identity_uri, 109 | authenticated=False) 110 | 111 | if not self._discover.url_for(version): 112 | # NOTE(jamielennox): The requested version is not supported by the 113 | # identity server. 114 | return None 115 | 116 | # NOTE(jamielennox): for backwards compatibility here we don't 117 | # actually use the URL from discovery we hack it up instead. :( 118 | # NOTE(blk-u): Normalizing the version is a workaround for bug 1450272. 119 | # This can be removed once that's fixed. Also fix the docstring for the 120 | # version parameter to be just "tuple". 121 | version = discover.normalize_version_number(version) 122 | if discover.version_match((2, 0), version): 123 | return '%s/v2.0' % self._identity_uri 124 | elif discover.version_match((3, 0), version): 125 | return '%s/v3' % self._identity_uri 126 | 127 | # NOTE(jamielennox): This plugin will only get called from auth_token 128 | # middleware. The middleware should never request a version that the 129 | # plugin doesn't know how to handle. 130 | msg = _('Invalid version asked for in auth_token plugin') 131 | raise NotImplementedError(msg) 132 | 133 | def invalidate(self): 134 | return self._plugin.invalidate() 135 | 136 | 137 | OPTS = [ 138 | cfg.StrOpt('auth_admin_prefix', 139 | default='', 140 | help='Prefix to prepend at the beginning of the path. ' 141 | 'Deprecated, use identity_uri.'), 142 | cfg.StrOpt('auth_host', 143 | default='127.0.0.1', 144 | help='Host providing the admin Identity API endpoint. ' 145 | 'Deprecated, use identity_uri.'), 146 | cfg.IntOpt('auth_port', 147 | default=35357, 148 | help='Port of the admin Identity API endpoint. ' 149 | 'Deprecated, use identity_uri.'), 150 | cfg.StrOpt('auth_protocol', 151 | default='https', 152 | choices=('http', 'https'), 153 | help='Protocol of the admin Identity API endpoint. ' 154 | 'Deprecated, use identity_uri.'), 155 | cfg.StrOpt('identity_uri', 156 | help='Complete admin Identity API endpoint. This ' 157 | 'should specify the unversioned root endpoint ' 158 | 'e.g. https://localhost:35357/'), 159 | cfg.StrOpt('admin_token', 160 | secret=True, 161 | help='This option is deprecated and may be removed in ' 162 | 'a future release. Single shared secret with the ' 163 | 'Keystone configuration used for bootstrapping a ' 164 | 'Keystone installation, or otherwise bypassing ' 165 | 'the normal authentication process. This option ' 166 | 'should not be used, use `admin_user` and ' 167 | '`admin_password` instead.'), 168 | cfg.StrOpt('admin_user', 169 | help='Service username.'), 170 | cfg.StrOpt('admin_password', 171 | secret=True, 172 | help='Service user password.'), 173 | cfg.StrOpt('admin_tenant_name', 174 | default='admin', 175 | help='Service tenant name.'), 176 | ] 177 | 178 | 179 | cfg.CONF.register_opts(OPTS, group=_base.AUTHTOKEN_GROUP) 180 | --------------------------------------------------------------------------------