././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3327944 keystoneauth1-4.4.0/0000775000175000017500000000000000000000000014346 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/.coveragerc0000664000175000017500000000014700000000000016471 0ustar00zuulzuul00000000000000[run] branch = True source = keystoneauth1 omit = keystoneauth1/tests/* [report] ignore_errors = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/.mailmap0000664000175000017500000000035400000000000015771 0ustar00zuulzuul00000000000000# Format is: # # ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/.stestr.conf0000664000175000017500000000007200000000000016616 0ustar00zuulzuul00000000000000[DEFAULT] test_path=./keystoneauth1/tests/unit top_dir=./ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/.zuul.yaml0000664000175000017500000000051300000000000016306 0ustar00zuulzuul00000000000000- project: templates: - check-requirements - lib-forward-testing-python3 - openstack-lower-constraints-jobs - openstack-python3-ussuri-jobs - openstacksdk-functional-tips - openstacksdk-tox-tips - osc-tox-unit-tips - publish-openstack-docs-pti - release-notes-jobs-python3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897232.0 keystoneauth1-4.4.0/AUTHORS0000664000175000017500000001744100000000000015425 0ustar00zuulzuul00000000000000Aaron Rosen Abhishek Kekane Adam Young Adrian Turjak Alan Pevec Alessio Ababilov Alessio Ababilov Alex Gaynor Alex Meade Alex Oughton Alex Schultz Alexander Makarov Alexey Stepanov Alvaro Lopez Garcia Andreas Jaeger Andrey Kurilin Antoni Segura Puimedon Antoni Segura Puimedon Ben Nemec Bernhard M. Wiedemann Bhuvan Arumugam Blake Covarrubias Boris Bobrov Boris Bobrov Boris Pavlovic Brant Knudson Brian Haley Brian Rosmaita Brian Waldon Cao Xuan Hoang Carlos D. Garza Chinmay Naik Chmouel Boudjnah Christian Berendt Chuck Short Clark Boylan Clenimar Sousa Clint Byrum Colleen Murphy Colleen Murphy Colleen Murphy Corey Bryant Cyril Roelandt Dan Prince Dave Chen David Stanek Dean Troyer Dirk Mueller Dmitry Tantsur Dmitry Tantsur Dolph Mathews Doug Fish Doug Hellmann Doug Hellmann Endre Karlson Eric Brown Eric Fried Eric Fried Eric Guo Federico Ressi Flavio Percoco Florent Flament Gabriel Hurley Gage Hugo Ghanshyam Mann Ghe Rivero Ghe Rivero Goutham Pacha Ravi Gregory Haynes Guang Yee Guang Yee Haiwei Xu Henry Nash Ian Cordasco Ian Cordasco Ihar Hrachyshka Ilya Kharin Jacek Tomasiak Jakub Ruzicka James E. Blair James E. Blair Jamie Lennox Jamie Lennox Jamie Lennox Jens Harbott Jens Rosenboom Jeremy Liu Jeremy Stanley Ji-Wei Joe Gordon Joe Gordon Joe Heck Joel Capitao John Dennis JordanP Jose Castro Leon Juan Antonio Osorio Robles Julien Danjou Kannan Manickam Ken'ichi Ohmichi Kristi Nikolla Kui Shi Lance Bragstad Lei Zhang Liem Nguyen Lin Hua Cheng LiuNanke Louis Taylor Luong Anh Tuan Marek Denis Mark McLoughlin Masayuki Igawa Mathieu Gagné Matt Mulsow Matt Riedemann Matthew Treinish Michael McCune Michael Solberg Mike Fedosin Monty Taylor Morgan Fainberg Navid Pustchi Ngo Quoc Cuong Ondřej Nový OpenStack Release Bot Pavlo Shchelokovskyy Pradeep Kilambi Prosunjit Biswas Q.hongtao Rabi Mishra Raildo Mascena Rodrigo Duarte Rodrigo Duarte Sousa Rodrigo Duarte Sousa Rodrigo Duarte Sousa Roxana Gherle Sam Morrison Samriddhi Samuel Pilla Samuel de Medeiros Queiroz Sascha Peilicke Sascha Peilicke Sean Dague Sean McGinnis Sean Perry Sergey Kraynev Sergio Cazzolato Steve Baker Steve Martinelli Steve Martinelli Steven Hardy Stuart McLaren Sushil Kumar Tang Chen TerryHowe Thierry Carrez Thomas Bechtold Thomas Goirand Tim Burke Tin Lam Tin Lam Tony Breeds Van Hung Pham Victor Morales Victor Stinner Vieri <15050873171@163.com> Vincent Untz Vishakha Agarwal Vishvananda Ishaya Vladyslav Drok Vu Cong Tuan Wu Wenxiang XiaojueGuan YangLei Yatin Kumbhare Yolanda Robla Zhenguo Niu ZhiQiang Fan ZhiQiang Fan Zhongyue Luo Ziad Sawalha ankitagrawal briancurtin caoyuan chioleong deepak_mourya deepakmourya gecong1973 gengchc2 guang-yee hgangwx howardlee huangtianhua jakedahn ji-xuepeng lilintan lin-hua-cheng lin-hua-cheng liuqing liushuobj llg8212 melissaml ricolin ricolin sridhargaddam wanghong wangqiangbj wangxiyuan wangxiyuan wangzhenyu zhangboye zhangguoqing zhangzs zhouxinyong ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/CONTRIBUTING.rst0000664000175000017500000000122300000000000017005 0ustar00zuulzuul00000000000000If you would like to contribute to the development of OpenStack, you must follow the steps documented at: https://docs.openstack.org/infra/manual/developers.html If you already have a good understanding of how the system works and your OpenStack accounts are set up, you can skip to the development workflow section of this documentation to learn how changes to OpenStack should be submitted for review via the Gerrit tool: https://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. Bugs should be filed on Launchpad, not GitHub: https://bugs.launchpad.net/keystoneauth ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897232.0 keystoneauth1-4.4.0/ChangeLog0000664000175000017500000012314000000000000016121 0ustar00zuulzuul00000000000000CHANGES ======= 4.4.0 ----- * Specify manila microversion header 4.3.0 ----- * Provide the default get\_auth\_ref implementation * Update master for stable/victoria 4.2.1 ----- * Correct major version discovery for non-keystone plugins * Fix docs builds for modern stevedors * Fix get\_endpoint\_data for non-keystone plugins 4.2.0 ----- * Drop python 3.5 support * Implement HTTP Basic client support in keystoneauth1 * Update lower-constraints versions 4.1.0 ----- * Remove mox3 from local upper-constraints * Replace assertItemsEqual with assertCountEqual * Add in-tree upper-constraints for py35 job * Make header Case Insensitive * Use unittest.mock instead of third party mock * Add py38 package metadata * Inject /v3 in token path for v3 plugins * Fix hacking min version to 3.0.1 * Fix E741 pep8 failure * Switch to newer openstackdocstheme and reno versions * Re-add python3.5 testing * Cleanup py27 support * Update master for stable/ussuri 4.0.0 ----- * Update hacking for Python3 * Remove universal wheel configuration * fix bandit complains * [ussuri][goal] Drop python 2.7 support and testing * Switch to Ussuri jobs 3.18.0 ------ * Fetch discovery documents with auth when needed * Make tests pass in 2020 * Generate pdf documentation * Follow the PTI for docs * Update master for stable/train * Fix misspell word 3.17.1 ------ * Simplify session logger object tests * Allow initializing session with connection retries * Cleanup session on delete 3.17.0 ------ * add support for auth\_receipts and multi-method auth 3.16.0 ------ * Add remove\_service to token fixtures * Allow requesting fixed retry delay instead of exponential 3.15.0 ------ * reno: per-request global\_request\_id * Add a per-request global\_request\_id * Add Python 3 Train unit tests * Cap bandit * Limit interval between retries to 1 minute * Allow setting retry counts for Adapter via configuration options * add a handler for unknown HTTP errors * add handling for multiple error returns * Replace git.openstack.org URLs with opendev.org URLs * Blacklist bandit 1.6.0 & cap sphinx for 2.7 * Resolves a typo in a link to use Application Credentials * OpenDev Migration Patch 3.14.0 ------ * Update auth plugin name list in document * Update the min version of tox * Factor Adapter conf-processing logic into a helper * Update master for stable/stein 3.13.1 ------ * Fix rate semaphore for keystoneclient * Drop py35 jobs 3.13.0 ------ * Add support for client-side rate limiting 3.12.0 ------ * Expose app creds and new attrs in fixtures * Remove shade jobs * add python 3.7 unit test job * Expose application credentials in AccessInfoV3 * Use template for lower-constraints * Change openstack-dev to openstack-discuss 3.11.2 ------ * Fix version discovery for clouds with int project\_ids * Replacing the HTTP protocal with HTTPS in using-sessions.rst * Add py36 tox environment * fix wrong spelling of "unnecessary" * Add missing release note for ironic discovery fix 3.11.1 ------ * Make new-style single endpoint version discovery actually work for ironic * Update sphinx extension logging * Reformat Adapter docstring * Cache root urls with and without trailing slashes 3.11.0 ------ * Protect against endpoint\_data not existing * Add support for ironic single-version responses * Remove os-testr from requirements * add lib-forward-testing-python3 test job * add python 3.6 unit test job * switch documentation job to new PTI * import zuul job settings from project-config * Add Keystone2Keystone example * Change log hashing to SHA256 * add release notes to readme.rst * Update reno for stable/rocky * Add ability to filter version data by service-type 3.10.0 ------ * Add netloc and version check for version discovery 3.9.0 ----- * Add minimum version for requirements * raise\_exc default in Adapter 3.8.0 ----- * fix tox python3 overrides * Add optional support for retrying certain HTTP codes 3.7.0 ----- * Add oslo.config option for split-loggers * Collect timing information for API calls * Return the endpoint\_override from EndpointData * Expose version\_between as a real function 3.6.2 ----- * Fix logging of encoded headers 3.6.1 ----- * Fix interface argument to get\_all\_version\_data 3.6.0 ----- * Reference class variable in Status * Use Status variables in tests * Infer version from old versioned service type aliases * Turn normalize\_status into a class * Make VersionData class * Add methods to get all of the version data * Implement service\_type alias lookups * Trivial: Update pypi url to new url * Allow tuples and sets in interface list 3.5.0 ----- * add lower-constraints job * Expose version status in EndpointData * Fix W503 line-break-before-binary-operator * fix a typo in session.py * Remove tox\_install.sh and align with constraints consumption * Updated from global requirements * Be more helpful when version discovery fails * Updated from global requirements * Update links in README * Add pep8 import order validation * Override support message in AuthorizationFailure class * Fix a spelling error * Zuul: Remove project name * Add some comments explaining split\_loggers flag logic * Remove PYTHONHASHSEED setting * Split request logging into four different loggers * Update reno for stable/queens * Updated from global requirements 3.4.0 ----- * Add documentation and release note for app creds * Add support for application credentials * Updated from global requirements * Implement system scope * Use stestr in tox.ini * Updated from global requirements * Fix masked variable name * Shift additional\_user\_agent in the stack * Updated from global requirements * Fix docs builds * Add osc, shade and sdk tips jobs * Mark SAML loader properties as required 3.3.0 ----- * Remove setting of version/release from releasenotes * Updated from global requirements * Updated from global requirements * Make none auth usable in CLI * Add EndpointData.\_\_str\_\_ for debugging * Correct docs usage of keystoneauth1 session * Updated from global requirements * Migrate to stestr * Add version discovery support to BaseAuthPlugin * Recommend unversioned for endpoint-override * Nits in using-sessions.rst * Extract iterables for test\_keystone\_version\_data * Sanely order assertion args in test\_discovery * Add tests for mutually exclusive [min|max]version * Make discover.\_version\_between more consistent * Add loading mock fixtures * Updated from global requirements 3.2.0 ----- * Set self.allow to a dict if None is passed * Fix docstring typo * Update discovery url normalization with catalog info * Add method to get the api major version * Don't use mutable defaults in allow arguments * Protect against missing interface attribute * Adapter.get\_conf\_options(deprecated\_opts) * Allow discovery URLs to have trailing slashes * Remove use of positional decorator * Fix exception message in adapter loading * Update reno for stable/pike * Updated from global requirements * Parameter to tune mutual authentication in kerberos * Add release note for 'none' auth plugin * Enable some off-by-default checks 3.1.0 ----- * get\_conf\_options can exclude deprecated opts * Record the discovered major api version * Add tests to specifically test trailing slash behavior 3.0.1 ----- * Ensure constructed urls have trailing '/'s * Handle pathless URLs 3.0.0 ----- * Add note to requirements.txt * Fixed minor review comments and updated docs * Make discover.version\_between private * Discourage 'version' and accept 'M.latest' * Add ability to specify a microversion in a request * Remove deprecated\_since for interface and fix text * Enable sphinx todo extension * Add support for next\_min\_version and not\_before * Remove kwargs from adapter.get\_endpoint\_data * Expose valid\_interfaces as a ListOpt for config files * Add min\_version and max\_version to adapter constructors * Minor cleanup * Add paragraph clarifying major and micro versions * Update docs and add a release note * Update URLs in documents according to document migration * Make Discover.version\_data accept null max\_version * Miscellaneous cleanup in discover.py * normalize\_version\_number([1]) => (1, 0) and docs * Fix \_run\_discovery caching * Nix EndpointData.get\_versioned\_data(authenticated) * Expand some discover.py docstrings * Change locations of docs for intersphinx * Clean up a few review nits * Switch from oslosphinx to openstackdocstheme * Ensure we discover only when we should * Refactor volume mock urls in identity\_common tests * Support a list of interface values * Optimize matching version no microversion needed * Plumb endpoint\_override through get\_endpoint\_data * Expose getting EndpointData on adapter and session * Add support for version ranges * Support explicitly requesting the 'latest' version * Add flags to turn discovery on and off * Updated from global requirements * Add url manipulation and microversion collection * Move version discovery logic to keystoneauth1.discover * Rework discovery cache * Using assertFalse(A) instead of assertEqual(False, A) * Updated from global requirements * Updated from global requirements * Optimize the link address * Pass kwargs to the plugin getter * Rework EndpointData construction to normalize catalog first * Add returning EndpointData objects from discovery * Port the missing version data discovery tests from ksc * Add 'none' identity plugin 2.21.0 ------ * Fix html\_last\_updated\_fmt for Python3 * Replace assertTrue(isinstance()) with assertIsInstance() * Updated from global requirements * Add global\_request\_id param to Adapter constructor * Trivial fix typo in document * Updated from global requirements * Allow setting EndpointReference in ADFSPassword * Updated from global requirements * Add ADFSPassword to keystoneauth1 entry points * Fix V3ADFSPassword retrieval of scoped token 2.20.0 ------ * Introduce keystoneauth1.loading.adapter * Uncomment warning-is-error for doc building * Updated from global requirements * Add bindep.txt file * Updated inconsistent value of scope parameter * Updated from global requirements 2.19.0 ------ * Remove unused test requirement pycrypto * Updated from global requirements * Prevent JSON decode errors in the pre-record hook * Allow users to specify request matchers in Betamax * Remove pbr warnerrors in favor of sphinx check * Allow users to specify a serializer easily * Allow new cassettes to be recorded via fixture * Update test requirement * Add an allow\_version\_hack flag to session and identity plugins * Include the auth\_url in the discovery failure message * Updated from global requirements * Fixed multiple target Auth warning in docstring * Use https for \*.openstack.org references * Fix ClientException message property not set properly * Update reno for stable/ocata 2.18.0 ------ * Correctly Omit Response Body in Debug Mode * Add test for header in Saml2 plugin * Use comma as separator in ECP Accept HTTP header * Replace yaml.load() with yaml.safe\_load() 2.17.0 ------ * Add a full listing of all auth plugins and there options * Only log application/json in session to start * X-Serivce-Token should be hashed in the log * Add \_\_ne\_\_ built-in function * Log request-id for each api call * Remove references to Python 3.4 * Prevent MemoryError when logging response bodies * Updated from global requirements * Removes unnecessary utf-8 encoding * Add Constraints support * Remove discover from test-requirements * Replace six.iteritems() with .items() * Don't issue deprecation warning when nesting adapters * Updated from global requirements 2.16.0 ------ * Show team and repo badges on README * Drop MANIFEST.in - it's not needed by pbr * Add reauthenticate to generic plugins * Using assertIsNotNone() instead of assertNotEqual(None) * Fix a typo in base.py * Correct betamax fixture for more strict IDNA * Updated from global requirements * Updated from global requirements * Updated from global requirements * mark a few oidc parameters as required * Updated from global requirements * Updated from global requirements * Updated from global requirements 2.15.0 ------ * Allow setting client\_name, client\_version on adapter * Show deprecation warning and limit features for KSC session * Don't use private testtools.test module * Prevent changing content type in request 2.14.0 ------ * be more explicit about connection errors * Fix a typo in opts.py * Fix a typo in base.py * Add a service token wrapper * Allow specifying client and service info to user\_agent * Enable release notes translation 2.13.0 ------ * Updated from global requirements * Implement caching for the generic plugins * Updated from global requirements * Updated from global requirements * Use mockpatch fixtures from fixtures * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Fix parameters for Kerberos Auth Plugin * Test that v3fedkerb plugin loads * Updated from global requirements * doc: remove unused import * Raise NotImplementedError instead of NotImplemented * standardize release note page ordering * Update reno for stable/newton 2.12.1 ------ * get\_endpoint should return None when no version found 2.12.0 ------ * Disables TCP\_KEEPCNT when using Windows Subsystem for Linux * Updated from global requirements * Allow identity plugins to discover relative version urls 2.11.1 ------ * add status code 308 to \_REDIRECT\_STATUSES * User-agent header shouldn't start with a space * Add tests for user-agent with odd sys.argv * Update the home-page in setup.cfg 2.11.0 ------ * Add tests for YamlJsonSerializer * Updated from global requirements * Don't include openstack/common in flake8 exclude list * Improve authentication plugins documentation * Add missing class name to tuple of public objects * Correctly report available for ADFS plugin * Updated from global requirements * Fix arguments to \_auth\_required() * Fix the doc error in "using-session" * Use assertEqual() instead of assertDictEqual() 2.10.0 ------ * Lazy load oauthlib for plugin loading * oidc: add missing 'OidcAccessToken' to \_\_all\_\_ * oidc: implement client\_credentials grant type * Fix ECP doc link in Saml2 Password class doc * Updated from global requirements * Fix link for "extras dependencies" in extras doc * Add pretty serializer for betamax fixture * Update hacking to global-requirements value * Use SAML2 requests plugin * Updated from global requirements * oidc: move the get\_unscoped\_auth\_ref into the base class * oidc: deprecate grant\_type argument * oidc: add discovery document support * Add additional\_headers to session and adapter * Add Python 3.5 classifier and venv * remove unused LOG * Updated from global requirements * Updated from global requirements * Add prompt parameter to Opt * Auth plugin for X.509 tokenless authentication * oidc: fix OpenID scope management * Add create\_plugin to loader 2.9.0 ----- * Updated from global requirements * move release note to correct directory * oidc: fix OpenID Connect scope option * oidc: add tests for plugin loader * Don't mock the session.request function * Updated from global requirements * oidc: refactor unit tests * Updated from global requirements * Fix code example for OAuth1 authentication * Add entrypoint for Federated Kerberos * Fix kerberos available property * Document named kerberos plugin * Support TOTP auth plugin * Make the kerberos plugin loadable * Add available flag to plugin loaders * Updated from global requirements * PEP257: Ignore D203 because it was deprecated * Updated from global requirements * Apply a heuristic for product name if a user\_agent is not provided 2.8.0 ----- * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Let Oidc\* auth plugins accept scope parameters as kwargs * Updated from global requirements * Update keystoneauth fixture to support v3 * Check that all defined entry points can be loaded * Use betamax hooks to mask fixture results * oidc: fix typo on class name * oidc: fix option definition * oidc: add OidcAccessToken class to authenticate reusing an access token * oidc: fix typo in docstring * oidc: DRY when obtaining an access token * oidc: DRY when obtaining a keystone token * oidc: Remove unused parameters in \_OidcBase * Add is\_domain to keystoneauth token 2.7.0 ----- * Updated from global requirements * Updated from global requirements * docstring: fix typo * Updated from global requirements * Add oauth plugin to keystoneauth * Expose is\_admin\_project in AccessInfo * Updated from global requirements * Expose allow parameters for URL discovery * Updated from global requirements * Refactor variables for fixture and service * Enable bandit * Lazy import oslo\_config as required * Add 303 as redirect code for k2k plugin * Updated from global requirements * Fix H405, D105, D200, and D203 PEP257 * Updated from global requirements * Fixing D204, D205, D208, and D211 pep8 * Fixing D301 docstring * Removing tox ignore D400 * Fix documentation example again * Include query string in request logging * Removing tox ignore D401 and make keystoneauth compliant 2.6.0 ----- * Documentation example fix * Allow to send different recorders to betamax * Fix doc build if git is absent * Updated from global requirements * Updated from global requirements 2.5.0 ----- * fix OrderedDict mutated during iteration * Fix for PEP8 violation - D202 (No blank lines allowed after function docstring.) * Examples for migration from keystoneclient * Renamed endpoint to interface in docstring * Keystoneauth Authentication Plugin doc typo * Allow seeing full token response when debug enabled * Update reno for stable/mitaka * Examples for kerberos and saml2 plugins * Adding authentication compatibility for OpenStackClient * Swap the order of username deprecation * Fix exported symbol in identity.v3 * Editorial nits for docs * Improve usability of docs * Add links to federation plugins * Remove unavailable parameter * Generate FederationBaseAuth constructor parameters * Update test run instructions * Fix typos and improve formatting in migrating.rst * Updated from global requirements * Updated from global requirements * Cleanup docstrings * Fix docstring in identity.v3.oidc module 2.3.0 ----- * Cleanup test-requirements.txt * Updated from global requirements * Allow parameter expansion in endpoint\_override * Updated from global requirements * Updated from global requirements * Updated from global requirements * Use positional library instead of our own copy * Remove argparse from requirements * HTTPError should contain 'retry\_after' parameter * Updated from global requirements * Remove keyring as a test-requiremnet * Mark password/secret options as secret * Replace deprecated library function os.popen() with subprocess 2.2.0 ----- * add release notes for ksa 2.2.0 * Updated from global requirements * Replace assertEqual(None, \*) with assertIsNone in tests * Updated from global requirements * Change LOG.warn to LOG.warning * Wrong usage of "a" * On Windows and OSX, the 'socket' python module does not contain the attributes TCP\_KEEPCNT or TCP\_KEEPINTVL, causing services consuming the library to malfunction * Updated from global requirements * Add betamax to test-requirements.txt * Add some documentation about migrating from ksc * Docstring corrections * Iterate over copy of session.adapters keys in Python2/3 * Provide a RFC 7231 compliant user agent string * Updated from global requirements * Deprecated tox -downloadcache option removed * small fix to missing parameters in documentation * Remove confusing documentation * Update coverage to keystoneauth1 * Updated from global requirements * Cleanups to K2K plugin * Get versioned url for K2K auth * Extract SAML fixtures into templates * Add BetaMax Fixture * Updated from global requirements 2.1.0 ----- * Add oslo.config compatibility attributes * Fix PyPI badges * Default for service service type should be empty * Put py34 first in the env order of tox * Updated from global requirements 2.0.0 ----- * Extract the rescoping logic from federation and K2K * Add release notes for keystoneauth * Updated from global requirements * Add argparse registration from Adapter objects * Fix PEP 257 error * Updated from global requirements * Remove Python 2.6 support * Add docstring validation * Tweak the way plugin attributes are loaded * Add documentation to Opt * Add load\_from\_options\_getter to plugin loading * Cleanup time patching * Updated from global requirements * Put Session options into an option group * Address hacking check H405 * Allow saving and caching the plugin auth state * Add three pieces of the Session interface * Updated from global requirements * Specify default\_domain to generic plugin * Migrate kerberos plugin * Use XML matcher * Add XML matcher * Updated from global requirements * Remove mox * Split ADFS and SAML2 plugins * SAML2 authentication plugins in keystoneauth * Correct references in authentication-plugin.rst * Declare an extras directory for plugins * Updated from global requirements * Make public the base loader classes * Refactored AccessInfo.project\_scoped accessor 1.2.0 ----- * Expose bind data via AccessInfo * Return None from generic plugin if failure * Updated from global requirements * Copy AccessInfo tests from keystoneclient * Fix deprecated options in oslo\_config * Updated from global requirements * Add url as a deprecated alias for endpoint * Updated from global requirements * auto-generate release history * Make RST section delineation length match title * Remove "Features" section from README * Update the project description * Make \_\_all\_\_ immutable * Add UnknownConnectionError to \_\_all\_\_ * remove references to keystone CLI * Add shields.io version/downloads links/badges into README.rst * Allow fetching oslo.config Opts from plugins * Fix doc session example * add openid connect plugins * Change ignore-errors to ignore\_errors * Updated from global requirements 1.1.0 ----- * Updated from global requirements * There is no token\_endpoint.TokenEndpoint * Use option dest rather than name in missing error * Move generic loading tests into loading folder * Move session loading tests into loading section * Identity plugin thread safety * Fix typo in Python package summary in setup.cfg * Convert project to os-testr 1.0.0 ----- * Change the README to remove the warning for 1.0.0 release * Add accessor method for raw catalog content * Move around the tests so they can be found easier * Raise exception for v2 with domain scope * Change auth plugin help text to auth type * Use auth\_type instead of auth\_plugin by default * Add session and auth loading to loading.\_\_init\_\_ * Auth-url is required for identity plugins * Return oslo.config opts from config loading * Move admin\_token to base \_plugins dir * Mark tenant-name and tenant-id deprecated * Raise error if loader is provided name without id * Provide has\_scope\_parameters function on plugins * Remove deprecated options from identity base plugin * get\_available\_loaders should return loader object * Change option requirement testing * Better isolate loading tests * Update k2k plugin with related code comments * Remove plugin.load\_from\_conf\_options and argparse * Give easy entry points for session loading * Updated from global requirements 0.4.0 ----- * Fix plugin loading tests * Keep a consistent logger name for keystoneauth * Fix v2 plugin loaders to not have V2 prefix * Do not log binary data during debug * Clean up exception messages * Port in the argument scrubbing from OCC * Add required property to Opt class * Rename the actual plugin class to AdminToken * Expose admin\_token plugin * Use human readable exception messages * Updated from global requirements * Replace endpoint\_type with interface in catalog * Remove service\_type requirement from catalog searching * Allow searching a catalog on service or endpoint id * Import service catalog tests from keystoneclient * Make missingproperty private * Move AccessInfo objects into own module * Add role\_ids, role\_names to v3 fixture * Update .gitignore * Move session loading into loading module * Remove oslo\_config from auth plugin loading * Split plugin loading * Remove auth/ directory * Updated from global requirements * py34 not py33 is tested and supported * Updated from global requirements * Set reasonable defaults for TCP Keep-Alive * Fix decorators of properties in AccessToken * Expose bug in AccessToken * Updated from global requirements 0.3.0 ----- * Remove opestack-common.conf * Move to the keystoneauth1 namespace * Remove catalog/translation targets from tox.ini * Remove keystoneclient lingering files * Keystone2KeystoneAuthPlugin scoping capabilities * Add Keystone2KeystoneAuthPlugin for K2K federation * Support discovery on the AUTH\_INTERFACE * Remove unused fixtures * Add get\_communication\_params interface to plugins * Make normalize\_endpoint\_type public * Make \_is\_endpoint\_type\_match function public * Remove \_get\_service\_endpoints function * Use random strings for test fixtures * Stop using function deprecated in Python 3 * Cleanup fixture imports * Provide a means to get all installed plugins * Ensure that failing responses are logged * Typo in openstack client help * Drop use of 'oslo' namespace package * Remove functional tests from tox * Removes temporary fix for doc generation * Properly handle Service Provider in token fixtures * Fetch Service Providers urls from auth plugins * removed custom assertDictEqual * Encapsulate Service Providers in AccessInfo * Add protocol docstring in FederationBaseAuthPlugin * Add default domain to fixture.v3.V3FederationToken * Honour \`\`service\_providers\`\` in AccessInfo * Cleanup needless variable binding * Remove i18n stub * Rename federated.py to federation.py 0.2.0 ----- * Remove lxml test-requirement * Remove oslo.i18n dependency * Replace datetime calculations with utility functions * Remove oslo.utils dependency * Make utils file private * Remove old request method * Remove some cruft from the service catalog * Remove oslo serialization dependency * Remove un-needed requirements * Cleanup deprecated session variables * Remove session.construct * Move catalog hack functions to keystoneauth * Remove pbr as runtime depend 0.1.0 ----- * Add a readme * Reorder catalog exceptions * Remove unused plugins from entrypoints * Add endpoint and service ids to fixtures * Copy missed test\_fixtures from keystoneclient * Don't return default for domain in v2 accessinfo * Base Documentation changes * AccessInfo is not a dict * Make ServiceCatalog take an actual catalog * Remove the factory from service catalog * Don't save version into the dictionary * Cannot retrieve a token from service catalog * Remove region\_name from service catalog * Remove the AccessInfo Factory * Remove region\_name from catalog * Remove auth\_url property from AccessInfo * Remove management\_url from AccessInfo * Remove cli functions from utils * Rename \_discover module * Reorganize exceptions * Change keystoneclient to keystoneauth in docs * Add doc data back in. This will be replaced by real doc data * Fix namespace issues and remove tests for client-specific code * update \_discover to use keystoneauth namespace * move keystoneclient/\_discover.py to keystoneauth/\_discover.py * Initial Split of python-keystoneclient to keystoneauth * Uncap library requirements for liberty * Make process\_header private * Increase minimum token life required * Allow requesting an unscoped Token * Expose audit\_id via AccessInfo * Replace assertRaisesRegexp with assertRaisesRegex * Updated from global requirements * Return None for missing trust\_id in fixture * Improve feedback message in SSL error * Add a FederatedBase v3 plugin * Rename requests mock object in testing * Provide a generic auth plugin loader * Extract BaseAuth out of Auth Plugin * Split v3 authentication file into module * Allow passing logger object to request * Implements subtree\_as\_ids and parents\_as\_ids * Updated from global requirements * Allow handling multiple service\_types * Updated from global requirements * Add default body for non-abstract empty methods * Create functional test base * Ignore all failures removing catalog when logging token * Using correct keyword for region in v3 * Move tests to the unit subdirectory * Make remove\_service\_catalog private * Change hacking check to verify all oslo imports * Change oslo.i18n to oslo\_i18n * Workflow documentation is now in infra-manual * Basic AccessInfo plugin * Enable hacking rule E122 and H304 * Add get\_headers interface to authentication plugins * Add name parameter to NoMatchingPlugin exception * Change oslo.config to oslo\_config * Change oslo.serialization to oslo\_serialization * Switch from oslo.utils to oslo\_utils * Updated from global requirements * Surface the user\_id and project\_id beyond the plugin * Configure TCP Keep-Alive for certain Sessions * Correct failures for check H238 * Enable hacking rule F821 * Add auth plugin params to doc * Correct failures for check W292 * Move to hacking 0.10 * Updated from global requirements * don't log service catalog in every token response * Updated from global requirements * Allow fetching user\_id/project\_id from auth * Reference identity plugins from \_\_init\_\_.py * Allow v3 plugins to opt out of service catalog * Updated from global requirements * Fix up types within API documentation * Expose version matching functions to the public * Take plugin params from ENV rather than default * get\_endpoint should return the override * Pass all adapter parameters through to adapter * Correct documenting constructor parameters * Correct Session docstring * Add missing user-id option to generic.Password * duplicate auth-url option returned by BaseGenericPlugin * Fix importing config module and classmethod params * Curl statements to include globoff for IPv6 URLs * Updated from global requirements * Updated from global requirements * I18n * I18n * Correct use of noqa * Sync oslo-incubator to 1fc3cd47 * Log the CA cert with the debug statement * Use oslo\_debug\_helper and remove our own version * Updated from global requirements * Cleanup docs - raises class * Docstring cleanup for return type * Docstring cleanup for return type * Docstrings should have :returns: everywhere * Docstrings should have :returns: everywhere * Use oslo.utils and oslo.serialization * Log token with sha1 * Redact x-subject-token from response headers * Update hacking to 0.9.x * Updated from global requirements * Handle federated tokens * SAML2 federated authentication for ADFS * Allow retrying some failed requests * Versioned Endpoint hack for Sessions * Versioned Endpoint hack for Sessions * Pass kwargs to auth plugins * Sync with latest oslo-incubator * fix typos * Work toward Python 3.4 support and testing * warn against sorting requirements * Version independent plugins * Expose auth methods on the adapter * Add version parameter to adapter * Allow providing an endpoint\_override to requests * Allow passing None for username in v2.Password * Distinguish between name not provided and incorrect * Move fake session to HTTPClient * Allow providing a default value to CLI loading * Allow unauthenticated discovery * Allow unauthenticated discovery * Remove cruft from setup.cfg * Unsort pbr and hacking in requirements files * Add v3scopedsaml entry to the setup.cfg * Fix handling of deprecated opts in CLI * Updated from global requirements * Revert "Add oslo.utils requirement" * Revert "Use oslo.utils" * Remove lxml as a forced depend * Allow passing user\_id to v2Password plugin * Make auth plugins dest save to os\_ * Allow registering individual plugin CONF options * Standardize AccessInfo token setting * Individual plugin CLI registering * Mark auth plugin options as secret * Handle invalidate in identity plugins correctly * Isolate get\_discovery function * Use oslo.utils * Add oslo.utils requirement * Control identity plugin reauthentication * Config fixture from oslo-incubator is not used * Redact tokens in request headers * Convert httpretty to requests-mock * Updated from global requirements * Add the 'auth' interface type * Use oslosphinx to generate doc theme * Don't log sensitive auth data * Fix mistakes in token fixtures * SAML2 ECP auth plugin * remove useless part of error message * Use jsonutils to load adapter response * Provide an \_\_all\_\_ for auth module * Allow loading auth plugins from CLI * Plugin loading from config objects * Ensure no double slash in get token URL * Add profiling support to keystoneclient * Add CONTRIBUTING.rst * Add a fixture for Keystone version discovery * Sync with oslo-incubator fd90c34a9 * Session loading from CLI options * Session loading from conf * Add trust users to AccessInfo and fixture * Add OAuth data to AccessInfo * Updated from global requirements * Add invalidate doc string to identity plugin * Session Adapters * Unversioned endpoints in service catalog * Unversioned endpoints in service catalog * Update keystoneclient code to account for hacking 0.9.2 * Rename v3.\_AuthConstructor to v3.AuthConstructor * Add issued handlers to auth\_ref and fixtures * Add role ids to the AccessInfo * Doc build fails if warnings * Updated from global requirements * Add service\_name to URL discovery * Remove \_factory methods from auth plugins * Fix tests to use UUID strings rather than ints for IDs * Sync with oslo-incubator caed79d * Add endpoint handling to Token/Endpoint auth * Remove left over vim headers * Add /role\_assignments endpoint support * Auth Plugin invalidation * Updated from global requirements * Fixes an erroneous type check in a test * Mark keystoneclient as being a universal wheel * Compressed Signature and Validation * OAuth request/access token and consumer support for oauth client API * Add mailmap entry * Sync with oslo-incubator 2640847 * Discovery URL querying functions * Remove importutils from oslo config * fixed typos found by RETF rules * Fix docstrings in keystoneclient * Synced jsonutils from oslo-incubator * Updated from global requirements * Create a V3 Token Generator * Add new error for invalid response * Rename HTTPError -> HttpError * Don't use generic kwargs in v2 Token Generation * Allow session to return an error response object * Updated from global requirements * Add service name to catalog * Hash functions support different hash algorithms * Allow passing auth plugin as a parameter * Ensure JSON headers in Auth Requests * Create a test token generator and use it * Reuse module \`exceptions\` from Oslo * Updated from global requirements * Handle URLs via the session and auth\_plugins * Start using positional decorator * Fix passing get\_token kwargs to get\_access * Sync config fixture object from oslo.incubator * Add a positional decorator * Don't use a connection pool unless provided * Revert "Add request/access token and consumer..." * Revert "Authenticate via oauth" * Fix doc build errors * Generate module docs * Authenticate via oauth * Add request/access token and consumer support for keystoneclient * Handle Token/Endpoint authentication * Updated from global requirements * Provide more data to AuthMethod plugins * Enforce scope mutual exclusion for trusts * Privatize auth construction parameters * Remove dependent module py3kcompat * Create V3 Auth Plugins * Create V2 Auth Plugins * Fix role\_names call from V3 AccessInfo * Add Python 3 classifiers * Remove tox locale overrides * Remove vim header * Sync openstack/common/memorycache.py with Oslo * HTTPretty: Bump to 0.8.0 * Update my mailmap * Sync apiclient from oslo * Create Authentication Plugins * Fix debug curl commands for included data * Add back --insecure option to CURL debug * Provide a conversion function for creating session * Return role names by AccessInfo.role\_names * Fix typos in documents and comments * Using common method 'bool\_from\_string' from oslo strutils * Sort items in requirement related files * Adjust import items according to hacking import rule * Sync with global requirements * Updated from global requirements * Saner debug log message generation * Controllable redirect handling * Revert "Whitelist external netaddr requirement" * Sync strutils from oslo * Fix auth\_token middleware test invalid cross-device link issue * Debug env for tox * Whitelist external netaddr requirement * Do not try to call decode() on a text string * Move redirect handling to session * Remove debug specific handling * Update requirements * HTTPretty: update to 0.7.1 * Don't install pre-release software with tox * Sync global requirements to pin sphinx to sphinx>=1.1.2,<1.2 * Allow commit title messages to end with a period * Sync with latest module from oslo * Properly handle Regions in keystoneclient * Discover supported APIs * Bump hacking to 0.8 * Updates .gitignore * Updates .gitignore * Extract a base Session object * Reorganize Service Catalog * Fix typo in keystoneclient * Encode the text before print it to console * Add testresources test requirement * Update tox.ini to usedevelop * Make HACKING.rst DRYer and turn into rst file * python3: Work around httpretty issue * Remove unused simplejson requirement * Migrate the keystone.common.cms to keystoneclient * Replace OpenStack LLC with OpenStack Foundation * Sync jsonutils from oslo * python3: Refactor dict for python2/python3 compat * Updated from global requirements * python3: Make iteritems py3k compat * Fix H202 assertRaises Exception * Allow v2 client authentication with trust\_id * Require oslo.config 1.2.0 final * Move tests in keystoneclient * Change Babel to a runtime requirement * Decode the non-english username str to unicode * Allow Hacking 0.7.x or later * Fixing potential NameErrors * Fix and enable gating on F811 * Fix and enable gating on F841 * Remove duplicate method in AccessInfo * remove the UUID check for userids * Fix and enable gating on H302: only import modules * Fix License Headers and Enable Gating on H102 * Add domain attributes to accessinfo * Support older token formats for projects in accessinfo * python3: Transition to mox3 instead of mox * Sync py3kcompat from oslo-incubator * Update oslo.config * Initial Trusts support * Add importutils and strutils from oslo * Synchronize code from oslo * Add apiclient.exceptions hierarchy * Fix and enable Gating on H404 * flake8: enable H201, H202, H802 * Add a get\_data function to Service Catalog * Extract basic request call * Updated from global requirements * flake8: fix alphabetical imports and enable H306 * Add discover to test-requirements * Update openstack-common.conf format * Fix and enable gating on H403 * Fix and enable gating on H402 * Use ServiceCatalog.factory, the object has no \_\_init\_\_ * Sync install\_venv\_common from oslo * Flake8 should ignore build folder * Fix and enable H401 * Use Python 3.x compatible print syntax * Implements v3 auth client * Use AuthRef for some client fields * Fix unused imports(flake8 F401, F999) * Fix line continuations (flake8 E125, E126) * python3: Introduce py33 to tox.ini * Enumerate ignored flake8 rules * Rename requires files to standard names * Allow secure user password update * Migrate to flake8 * Migrate to pbr * Use testr instead of nose * Perform oslo-incubator code sync * Added Conflict Exception to the exception code map * Restore compatibility with PrettyTable < 0.7.2 * Sync memorycache and timeutils from oslo * Improve error message for missing endpoint * Use oslo-config-2013.1b3 * Use install\_venv\_common.py from oslo * Update .coveragerc * Treat HTTP code 400 and above as error * Fix STALE\_TOKEN\_DURATION usage * Factorize endpoint retrieval in access * Take region\_name into account when calling url\_for * Remove useless code * Add name arguments to keystone command * Add file 'ChangeLog' to MANIFEST.in * Use requests module for HTTP/HTTPS * Fix keystoneclient user-list output order * bug-1040361: use keyring to store tokens * Add --version CLI opt and \_\_version\_\_ module attr * updating PEP8 to 1.3.3 * Correct a misspelled in comments * Add auth-token code to keystoneclient, along with supporting files * Make initial structural changes to keystoneclient in preparation to moving auth\_token here from keystone. No functional change should occur from this commit (even though it did refresh a newer copy of openstack.common.setup.py, none of the newer updates are in functions called from this client) * removing repeat attempt at authorization in client * HACKING compliance: consistent usage of 'except' * Replace refs to 'Keystone API' with 'Identity API' * Don't log an exception for an expected empty catalog * Add OpenStack trove classifier for PyPI * Useful error msg when missing catalog (bug 949904) * v3 Domain/Project role grants * updating keystoneclient doc theme * enabling i18n with Babel * pep8 1.3.1 cleanup * Add wrap option to keystone token-get for humans * Fixes setup compatibility issue on Windows * Handle "503 Service Unavailable" exception * Support 2-way SSL with Keystone server if it is configured to enforce 2-way SSL. See also https://review.openstack.org/#/c/7706/ for the corresponding review for the 2-way SSL addition to Keystone * Don't call PrettyTable add\_row with a tuple * Add post-tag versioning * Do not display None in pretty tables for fields with no value * Skip argparse when injecting requirements * Update to latest openstack.common setup code * Move docs to doc * pep8 1.1 changes and updates * Remove printt * Auto generate AUTHORS for python-keystoneclient * Include last missing files in tarball * Open Folsom * Updated tox.ini to work properly with Jenkins * Backslash continuations (python-keystoneclient) * enabled treated as string (bug 953678) * Make ec2-credentials-\* commands work properly for non-admin user * Improve usability of CLI * Added condition requirement to argparse * Display token and service catalog for user * Add license file to the tarball * Updates client to work with keystone essex roles API routes * Support for version and extension discovery * Adjust version number to match other deliveries * Modify tox.ini file to do the standard thigns * Added in common test, venv and gitreview stuff * Improved error message when unable to communicate with keystone * Improved logging/error messages * Initial commit * Initial commit ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/HACKING.rst0000664000175000017500000000122100000000000016140 0ustar00zuulzuul00000000000000Keystone Style Commandments =========================== - Step 1: Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ - Step 2: Read on Exceptions ---------- When dealing with exceptions from underlying libraries, translate those exceptions to an instance or subclass of ClientException. ======= Testing ======= keystoneauth uses testtools and stestr for its unittest suite and its test runner. Basic workflow around our use of tox and stestr can be found at https://wiki.openstack.org/testr. If you'd like to learn more in depth: https://testtools.readthedocs.io/en/latest/ https://stestr.readthedocs.io/en/latest/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/LICENSE0000664000175000017500000002716300000000000015364 0ustar00zuulzuul00000000000000Copyright (c) 2009 Jacob Kaplan-Moss - initial codebase (< v2.1) Copyright (c) 2011 Rackspace - OpenStack extensions (>= v2.1) Copyright (c) 2011 Nebula, Inc - Keystone refactor (>= v2.7) All rights reserved. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. --- License for keystoneauth versions prior to 2.1 --- All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of this project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3327944 keystoneauth1-4.4.0/PKG-INFO0000664000175000017500000000475700000000000015460 0ustar00zuulzuul00000000000000Metadata-Version: 2.1 Name: keystoneauth1 Version: 4.4.0 Summary: Authentication Library for OpenStack Identity Home-page: https://docs.openstack.org/keystoneauth/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/keystoneauth.svg :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on ============ keystoneauth ============ .. image:: https://img.shields.io/pypi/v/keystoneauth1.svg :target:https://pypi.org/project/keystoneauth1 :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/keystoneauth1.svg :target: https://pypi.org/project/keystoneauth1/ :alt: Downloads This package contains tools for authenticating to an OpenStack-based cloud. These tools include: * Authentication plugins (password, token, and federation based) * Discovery mechanisms to determine API version support * A session that is used to maintain client settings across requests (based on the requests Python library) Further information: * Free software: Apache license * Documentation: https://docs.openstack.org/keystoneauth/latest/ * Source: https://opendev.org/openstack/keystoneauth * Bugs: https://bugs.launchpad.net/keystoneauth * Release notes: https://docs.openstack.org/releasenotes/keystoneauth/ Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Requires-Python: >=3.6 Provides-Extra: betamax Provides-Extra: kerberos Provides-Extra: oauth1 Provides-Extra: saml2 Provides-Extra: test ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/README.rst0000664000175000017500000000224700000000000016042 0ustar00zuulzuul00000000000000======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/keystoneauth.svg :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on ============ keystoneauth ============ .. image:: https://img.shields.io/pypi/v/keystoneauth1.svg :target:https://pypi.org/project/keystoneauth1 :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/keystoneauth1.svg :target: https://pypi.org/project/keystoneauth1/ :alt: Downloads This package contains tools for authenticating to an OpenStack-based cloud. These tools include: * Authentication plugins (password, token, and federation based) * Discovery mechanisms to determine API version support * A session that is used to maintain client settings across requests (based on the requests Python library) Further information: * Free software: Apache license * Documentation: https://docs.openstack.org/keystoneauth/latest/ * Source: https://opendev.org/openstack/keystoneauth * Bugs: https://bugs.launchpad.net/keystoneauth * Release notes: https://docs.openstack.org/releasenotes/keystoneauth/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/bindep.txt0000664000175000017500000000076700000000000016362 0ustar00zuulzuul00000000000000# This is a cross-platform list tracking distribution packages needed for install and tests; # see https://docs.openstack.org/infra/bindep/ for additional information. build-essential [platform:dpkg test] python-dev [platform:dpkg test] python-devel [platform:rpm test] libkrb5-dev [platform:dpkg test] krb5-devel [platform:rpm test] libxml2-dev [platform:dpkg] libxml2-devel [platform:rpm] libxslt-devel [platform:rpm] libxslt1-dev [platform:dpkg] zlib-devel [platform:rpm] zlib1g-dev [platform:dpkg] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2727942 keystoneauth1-4.4.0/doc/0000775000175000017500000000000000000000000015113 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/doc/.gitignore0000664000175000017500000000000700000000000017100 0ustar00zuulzuul00000000000000build/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/doc/Makefile0000664000175000017500000000614600000000000016562 0ustar00zuulzuul00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXSOURCE = source PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SPHINXSOURCE) .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/keystoneauth.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/keystoneauth.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2727942 keystoneauth1-4.4.0/doc/ext/0000775000175000017500000000000000000000000015713 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/doc/ext/__init__.py0000664000175000017500000000000000000000000020012 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/doc/ext/list_plugins.py0000664000175000017500000000577500000000000021017 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import inspect from docutils import nodes from docutils.parsers import rst from docutils.parsers.rst import directives from docutils.statemachine import ViewList from sphinx.util import logging from sphinx.util.nodes import nested_parse_with_titles from stevedore import extension LOG = logging.getLogger(__name__) class ListAuthPluginsDirective(rst.Directive): """Present a simple list of the plugins in a namespace.""" option_spec = { 'class': directives.class_option, 'overline-style': directives.single_char_or_unicode, 'underline-style': directives.single_char_or_unicode, } has_content = True def report_load_failure(mgr, ep, err): LOG.warning(u'Failed to load %s: %s' % (ep.module_name, err)) def display_plugin(self, ext): overline_style = self.options.get('overline-style', '') underline_style = self.options.get('underline-style', '=') if overline_style: yield overline_style * len(ext.name) yield ext.name if underline_style: yield underline_style * len(ext.name) yield "\n" doc = inspect.getdoc(ext.obj) if doc: yield doc yield "\n" yield "------" yield "\n" for opt in ext.obj.get_options(): yield ":%s: %s" % (opt.name, opt.help) yield "\n" def run(self): mgr = extension.ExtensionManager( 'keystoneauth1.plugin', on_load_failure_callback=self.report_load_failure, invoke_on_load=True, ) result = ViewList() for name in sorted(mgr.names()): for line in self.display_plugin(mgr[name]): for l in line.splitlines(): ep = mgr[name] try: module_name = ep.entry_point.module_name except AttributeError: try: module_name = ep.entry_point.module except AttributeError: module_name = ep.entry_point.value result.append(l, module_name) # Parse what we have into a new section. node = nodes.section() node.document = self.state.document nested_parse_with_titles(self.state, result, node) return node.children def setup(app): LOG.info('loading keystoneauth1 plugins') app.add_directive('list-auth-plugins', ListAuthPluginsDirective) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/doc/requirements.txt0000664000175000017500000000112600000000000020377 0ustar00zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. # For generationg sphinx documentation openstackdocstheme>=2.2.1 # Apache-2.0 reno>=3.1.0 # Apache-2.0 sphinx>=2.0.0,!=2.1.0 # BSD sphinxcontrib-apidoc>=0.2.0 # BSD # For autodoc builds fixtures>=3.0.0 # Apache-2.0/BSD betamax>=0.7.0 # Apache-2.0 oslo.config>=5.2.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 lxml>=4.2.0 # BSD oauthlib>=0.6.2 # BSD ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2727942 keystoneauth1-4.4.0/doc/source/0000775000175000017500000000000000000000000016413 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/doc/source/authentication-plugins.rst0000664000175000017500000004727700000000000023664 0ustar00zuulzuul00000000000000====================== Authentication Plugins ====================== Introduction ============ Authentication plugins provide a generic means by which to extend the authentication mechanisms known to OpenStack clients. In the vast majority of cases the authentication plugins used will be those written for use with the OpenStack Identity Service (Keystone), however this is not the only possible case, and the mechanisms by which authentication plugins are used and implemented should be generic enough to cover completely customized authentication solutions. The subset of authentication plugins intended for use with an OpenStack Identity server (such as Keystone) are called Identity Plugins. Available Plugins ================= Keystoneauth ships with a number of plugins and particularly Identity Plugins. V2 Identity Plugins ------------------- Standard V2 identity plugins are defined in the module: :py:mod:`keystoneauth1.identity.v2` They include: - :py:class:`~keystoneauth1.identity.v2.Password`: Authenticate against a V2 identity service using a username and password. - :py:class:`~keystoneauth1.identity.v2.Token`: Authenticate against a V2 identity service using an existing token. V2 identity plugins must use an `auth_url` that points to the root of a V2 identity server URL, i.e.: ``http://hostname:5000/v2.0``. V3 Identity Plugins ------------------- Standard V3 identity plugins are defined in the module :py:mod:`keystoneauth1.identity.v3`. V3 Identity plugins are slightly different from their V2 counterparts as a V3 authentication request can contain multiple authentication methods. To handle this V3 defines a number of different :py:class:`~keystoneauth1.identity.v3.AuthMethod` classes: - :py:class:`~keystoneauth1.identity.v3.PasswordMethod`: Authenticate against a V3 identity service using a username and password. - :py:class:`~keystoneauth1.identity.v3.TokenMethod`: Authenticate against a V3 identity service using an existing token. - :py:class:`~keystoneauth1.identity.v3.ReceiptMethod`: Authenticate against a V3 identity service using an existing auth-receipt. This method has to be used in conjunction with at least one other method. - :py:class:`~keystoneauth1.identity.v3.TOTPMethod`: Authenticate against a V3 identity service using Time-Based One-Time Password (TOTP). - :py:class:`~keystoneauth1.identity.v3.TokenlessAuth`: Authenticate against a V3 identity service using tokenless authentication. - :py:class:`~keystoneauth1.identity.v3.ApplicationCredentialMethod`: Authenticate against a V3 identity service using an application credential. - :py:class:`~keystoneauth1.extras.kerberos.KerberosMethod`: Authenticate against a V3 identity service using Kerberos. The :py:class:`~keystoneauth1.identity.v3.AuthMethod` objects are then passed to the :py:class:`~keystoneauth1.identity.v3.Auth` plugin:: >>> from keystoneauth1 import session >>> from keystoneauth1.identity import v3 >>> password = v3.PasswordMethod(username='user', ... password='password', ... user_domain_name='default') >>> auth = v3.Auth(auth_url='http://my.keystone.com:5000/v3', ... auth_methods=[password], ... project_id='projectid') >>> sess = session.Session(auth=auth) You can even add additional methods to an existing auth instance after it has been created:: >>> totp = v3.TOTPMethod(username='user', ... passcode='123456', ... user_domain_name='default') >>> auth.add_method(totp) Or use the :py:class:`~keystoneauth1.identity.v3.MultiFactor` helper plugin to do it all simply in one go, an example of whichs exists in the section below. For the common cases where you will only want to use one :py:class:`~keystoneauth1.identity.v3.AuthMethod` there are also helper authentication plugins for the various :py:class:`~keystoneauth1.identity.v3.AuthMethod` which can be used more like the V2 plugins: - :py:class:`~keystoneauth1.identity.v3.Password`: Authenticate using only a :py:class:`~keystoneauth1.identity.v3.PasswordMethod`. - :py:class:`~keystoneauth1.identity.v3.Token`: Authenticate using only a :py:class:`~keystoneauth1.identity.v3.TokenMethod`. - :py:class:`~keystoneauth1.identity.v3.TOTP`: Authenticate using only a :py:class:`~keystoneauth1.identity.v3.TOTPMethod`. - :py:class:`~keystoneauth1.extras.kerberos.Kerberos`: Authenticate using only a :py:class:`~keystoneauth1.extras.kerberos.KerberosMethod`. :: >>> auth = v3.Password(auth_url='http://my.keystone.com:5000/v3', ... username='username', ... password='password', ... project_id='projectid', ... user_domain_name='default') >>> sess = session.Session(auth=auth) This will have exactly the same effect as using the single :py:class:`~keystoneauth1.identity.v3.PasswordMethod` above. V3 identity plugins must use an `auth_url` that points to the root of a V3 identity server URL, i.e.: ``http://hostname:5000/v3``. Multi-Factor with V3 Identity Plugins ------------------------------------- The basic example of multi-factor authentication is when you supply all the needed auth methods up front. This can be done by building an Auth class with method instances: .. code-block:: python from keystoneauth1 import session from keystoneauth1.identity import v3 auth = v3.Auth( auth_url='http://my.keystone.com:5000/v3', auth_methods=[ v3.PasswordMethod( username='user', password='password', user_domain_id="default", ), v3.TOTPMethod( username='user', passcode='123456', user_domain_id="default", ) ], project_id='projectid', ) sess = session.Session(auth=auth) Or by letting the helper plugin do it for you: .. code-block:: python from keystoneauth1 import session from keystoneauth1.identity import v3 auth = v3.MultiFactor( auth_url='http://my.keystone.com:5000/v3', auth_methods=['v3password', 'v3totp'], username='user', password='password', passcode='123456', user_domain_id="default", project_id='projectid', ) sess = session.Session(auth=auth) **Note:** The :py:class:`~keystoneauth1.identity.v3.MultiFactor` helper does not support auth receipts as an option in auth_methods, but one can be added with `auth.add_method`. When you supply just one method when multiple are needed, a :py:class:`~keystoneauth1.exceptions.auth.MissingAuthMethods` error will be raised. This can be caught, and you can infer based on the error what the missing methods were, and from it extract the receipt to continue authentication: .. code-block:: python auth = v3.Password(auth_url='http://my.keystone.com:5000/v3', username='username', password='password', project_id='projectid', user_domain_id='default') sess = session.Session(auth=auth) try: sess.get_token() except exceptions.MissingAuthMethods as e: receipt = e.receipt methods = e.methods required_methods = e.required_auth_methods Once you know what auth methods are needed to continue, you can extend the existing auth plugin with additional methods: .. code-block:: python auth.add_method( v3.TOTPMethod( username='user', passcode='123456', user_domain_id='default', ) ) sess.get_token() Or if you do not have the existing auth method, but have the receipt you can continue as well: .. code-block:: python auth = v3.TOTP( auth_url='http://my.keystone.com:5000/v3', username='user', passcode='123456', user_domain_id='default', project_id='projectid', ) auth.add_method(v3.ReceiptMethod(receipt=receipt)) sess = session.Session(auth=auth) sess.get_token() Standalone Plugins ------------------ Services can be deployed in a standalone environment where there is no integration with an identity service. The following plugins are provided to support standalone services: - :py:class:`~keystoneauth1.http_basic.HTTPBasicAuth`: HTTP Basic authentication - :py:class:`~keystoneauth1.noauth.NoAuth`: No authentication Standalone plugins must be given an `endpoint` that points to the URL of the one service being used, since there is no service catalog to look up endpoints:: from keystoneauth1 import session from keystoneauth1 import noauth auth = noauth.NoAuth(endpoint='http://hostname:6385/') sess = session.Session(auth=auth) :py:class:`~keystoneauth1.http_basic.HTTPBasicAuth` also requres a `username` and `password`:: from keystoneauth1 import session from keystoneauth1 import http_basic auth = http_basic.HTTPBasicAuth(endpoint='http://hostname:6385/', username='myUser', password='myPassword') sess = session.Session(auth=auth) Federation ========== The following V3 plugins are provided to support federation: - :py:class:`~keystoneauth1.extras.kerberos.MappedKerberos`: Federated (mapped) Kerberos. - :py:class:`~keystoneauth1.extras._saml2.v3.Password`: SAML2 password authentication. - :py:class:`~keystoneauth1.identity.v3:OpenIDConnectAccessToken`: Plugin to reuse an existing OpenID Connect access token. - :py:class:`~keystoneauth1.identity.v3:OpenIDConnectAuthorizationCode`: OpenID Connect Authorization Code grant type. - :py:class:`~keystoneauth1.identity.v3:OpenIDConnectClientCredentials`: OpenID Connect Client Credentials grant type. - :py:class:`~keystoneauth1.identity.v3:OpenIDConnectPassword`: OpenID Connect Resource Owner Password Credentials grant type. - :py:class:`~keystoneauth1.identity.v3.Keystone2Keystone`: Keystone to Keystone Federation. The Keystone2Keystone plugin is special as it takes a Password auth for one keystone instance acting as an Identity Provider as input in order to create a session on the keystone acting as a Service Provider, for example: .. code-block:: python from keystoneauth1 import session from keystoneauth1.identity import v3 from keystoneauth1.identity.v3 import k2k pwauth = v3.Password(auth_url='http://my.keystone.com:5000/v3', username='username', password='password', project_id='projectid', user_domain_name='Default') k2kauth = k2k.Keystone2Keystone(pwauth, 'mysp', project_id='federated_projectid') k2ksession = session.Session(auth=k2kauth) Version Independent Identity Plugins ------------------------------------ Standard version independent identity plugins are defined in the module :py:mod:`keystoneauth1.identity.generic`. For the cases of plugins that exist under both the identity V2 and V3 APIs there is an abstraction to allow the plugin to determine which of the V2 and V3 APIs are supported by the server and use the most appropriate API. These plugins are: - :py:class:`~keystoneauth1.identity.generic.Password`: Authenticate using a user/password against either v2 or v3 API. - :py:class:`~keystoneauth1.identity.generic.Token`: Authenticate using an existing token against either v2 or v3 API. These plugins work by first querying the identity server to determine available versions and so the `auth_url` used with the plugins should point to the base URL of the identity server to use. If the `auth_url` points to either a V2 or V3 endpoint it will restrict the plugin to only working with that version of the API. Simple Plugins -------------- In addition to the Identity plugins a simple plugin that will always use the same provided token and endpoint is available. This is useful in situations where you have an token or in testing when you specifically know the endpoint you want to communicate with. It can be found at :py:class:`keystoneauth1.token_endpoint.Token`. V3 OAuth 1.0a Plugins --------------------- There also exists a plugin for OAuth 1.0a authentication. We provide a helper authentication plugin at: :py:class:`~keystoneauth1.extras.oauth1.V3OAuth1`. The plugin requires the OAuth consumer's key and secret, as well as the OAuth access token's key and secret. For example:: >>> from keystoneauth1.extras import oauth1 >>> from keystoneauth1 import session >>> a = oauth1.V3OAuth1('http://my.keystone.com:5000/v3', ... consumer_key=consumer_id, ... consumer_secret=consumer_secret, ... access_key=access_token_key, ... access_secret=access_token_secret) >>> s = session.Session(auth=a) Application Credentials ======================= There is a specific authentication method for interacting with Identity servers that support application credential authentication. Since application credentials are associated to a user on a specific project, some parameters are not required as they would be with traditional password authentication. The following method can be used to authenticate for a token using an application credential: - :py:class:`~keystoneauth1.identity.v3.ApplicationCredential`: The following example shows the method usage with a session:: >>> from keystoneauth1 import session >>> from keystone.identity import v3 >>> auth = v3.ApplicationCredential( application_credential_secret='application_credential_secret', application_credential_id='c2872b920853478292623be94b657090' ) >>> sess = session.Session(auth=auth) Tokenless Auth ============== A plugin for tokenless authentication also exists. It provides a means to authorize client operations within the Identity server by using an X.509 TLS client certificate without having to issue a token. We provide a tokenless authentication plugin at: - :class:`~keystoneauth1.identity.v3.TokenlessAuth` It is mostly used by service clients for token validation and here is an example of how this plugin would be used in practice:: >>> from keystoneauth1 import session >>> from keystoneauth1.identity import v3 >>> auth = v3.TokenlessAuth(auth_url='https://keystone:5000/v3', ... domain_name='my_service_domain') >>> sess = session.Session( ... auth=auth, ... cert=('/opt/service_client.crt', ... '/opt/service_client.key'), ... verify='/opt/ca.crt') Loading Plugins by Name ======================= In auth_token middleware and for some service to service communication it is possible to specify a plugin to load via name. The authentication options that are available are then specific to the plugin that you specified. Currently the authentication plugins that are available in `keystoneauth` are: - http_basic: :py:class:`keystoneauth1.http_basic.HTTPBasicAuth` - none: :py:class:`keystoneauth1.noauth.NoAuth` - password: :py:class:`keystoneauth1.identity.generic.Password` - token: :py:class:`keystoneauth1.identity.generic.Token` - v2password: :py:class:`keystoneauth1.identity.v2.Password` - v2token: :py:class:`keystoneauth1.identity.v2.Token` - v3applicationcredential: :py:class:`keystoneauth1.identity.v3.ApplicationCredential` - v3password: :py:class:`keystoneauth1.identity.v3.Password` - v3token: :py:class:`keystoneauth1.identity.v3.Token` - v3fedkerb: :py:class:`keystoneauth1.extras.kerberos.MappedKerberos` - v3kerberos: :py:class:`keystoneauth1.extras.kerberos.Kerberos` - v3oauth1: :py:class:`keystoneauth1.extras.oauth1.v3.OAuth1` - v3oidcaccesstoken: :py:class:`keystoneauth1.identity.v3:OpenIDConnectAccessToken` - v3oidcauthcode: :py:class:`keystoneauth1.identity.v3:OpenIDConnectAuthorizationCode` - v3oidcclientcredentials: :py:class:`keystoneauth1.identity.v3:OpenIDConnectClientCredentials` - v3oidcpassword: :py:class:`keystoneauth1.identity.v3:OpenIDConnectPassword` - v3samlpassword: :py:class:`keystoneauth1.extras._saml2.v3.Password` - v3tokenlessauth: :py:class:`keystoneauth1.identity.v3.TokenlessAuth` - v3totp: :py:class:`keystoneauth1.identity.v3.TOTP` Creating Authentication Plugins =============================== Creating an Identity Plugin --------------------------- If you have implemented a new authentication mechanism into the Identity service then you will be able to reuse a lot of the infrastructure available for the existing Identity mechanisms. As the V2 identity API is essentially frozen, it is expected that new plugins are for the V3 API. To implement a new V3 plugin that can be combined with others you should implement the base :py:class:`keystoneauth1.identity.v3.AuthMethod` class and implement the :py:meth:`~keystoneauth1.identity.v3.AuthMethod.get_auth_data` function. If your Plugin cannot be used in conjunction with existing :py:class:`keystoneauth1.identity.v3.AuthMethod` then you should just override :py:class:`keystoneauth1.identity.v3.Auth` directly. The new :py:class:`~keystoneauth1.identity.v3.AuthMethod` should take all the required parameters via :py:meth:`~keystoneauth1.identity.v3.AuthMethod.__init__` and return from :py:meth:`~keystoneauth1.identity.v3.AuthMethod.get_auth_data` a tuple with the unique identifier of this plugin (e.g. *password*) and a dictionary containing the payload of values to send to the authentication server. The session, calling auth object and request headers are also passed to this function so that the plugin may use or manipulate them. You should also provide a class that inherits from :py:class:`keystoneauth1.identity.v3.Auth` with an instance of your new :py:class:`~keystoneauth1.identity.v3.AuthMethod` as the `auth_methods` parameter to :py:class:`keystoneauth1.identity.v3.Auth`. By convention (and like above) these are named `PluginType` and `PluginTypeMethod` (for example :py:class:`~keystoneauth1.identity.v3.Password` and :py:class:`~keystoneauth1.identity.v3.PasswordMethod`). Creating a Custom Plugin ------------------------ To implement an entirely new plugin you should implement the base class :py:class:`keystoneauth1.plugin.BaseAuthPlugin` and provide the :py:meth:`~keystoneauth1.plugin.BaseAuthPlugin.get_endpoint`, :py:meth:`~keystoneauth1.plugin.BaseAuthPlugin.get_token` and :py:meth:`~keystoneauth1.plugin.BaseAuthPlugin.invalidate` methods. :py:meth:`~keystoneauth1.plugin.BaseAuthPlugin.get_token` is called to retrieve the string token from a plugin. It is intended that a plugin will cache a received token and so if the token is still valid then it should be re-used rather than fetching a new one. A session object is provided with which the plugin can contact it's server. (Note: use `authenticated=False` when making those requests or it will end up being called recursively). The return value should be the token as a string. :py:meth:`~keystoneauth1.plugin.BaseAuthPlugin.get_endpoint` is called to determine a base URL for a particular service's requests. The keyword arguments provided to the function are those that are given by the `endpoint_filter` variable in :py:meth:`keystoneauth1.session.Session.request`. A session object is also provided so that the plugin may contact an external source to determine the endpoint. Again this will be generally be called once per request and so it is up to the plugin to cache these responses if appropriate. The return value should be the base URL to communicate with. :py:meth:`~keystoneauth1.plugin.BaseAuthPlugin.invalidate` should also be implemented to clear the current user credentials so that on the next :py:meth:`~keystoneauth1.plugin.BaseAuthPlugin.get_token` call a new token can be retrieved. The most simple example of a plugin is the :py:class:`keystoneauth1.token_endpoint.Token` plugin. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/doc/source/conf.py0000664000175000017500000001662100000000000017720 0ustar00zuulzuul00000000000000# -*- coding: utf-8 -*- # # keystoneauth1 documentation build configuration file, created by # sphinx-quickstart on Sun Dec 6 14:19:25 2009. # # This file is execfile()d with the current directory set to its containing # dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys import pbr.version sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.intersphinx', 'openstackdocstheme', 'ext.list_plugins', 'sphinxcontrib.apidoc', ] # sphinxcontrib.apidoc options apidoc_module_dir = '../../keystoneauth1' apidoc_output_dir = 'api' apidoc_excluded_paths = [ 'hacking', 'tests/*', 'tests', 'test'] apidoc_separate_modules = True todo_include_todos = True # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = 'keystoneauth1' copyright = 'OpenStack Contributors' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # A list of ignored prefixes for module index sorting. modindex_common_prefix = ['keystoneauth1.'] # Grouping the document tree for man pages. # List of tuples 'sourcefile', 'target', 'title', 'Authors name', 'manual' #man_pages = [] # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. #html_theme_path = ["."] #html_theme = '_theme' html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['static'] # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'keystoneauthdoc' # -- Options for LaTeX output ------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]) # . latex_documents = [ ('index', 'doc-keystoneauth.tex', u'keystoneauth1 Documentation', u'Openstack Developers', 'manual', True), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True ksc = 'https://docs.openstack.org/python-keystoneclient/latest/' # Disable usage of xindy https://bugzilla.redhat.com/show_bug.cgi?id=1643664 latex_use_xindy = False latex_domain_indices = False latex_elements = { 'makeindex': '', 'printindex': '', 'preamble': r'\setcounter{tocdepth}{3}', } intersphinx_mapping = { 'python': ('http://docs.python.org/', None), 'osloconfig': ('https://docs.openstack.org/oslo.config/latest/', None), 'keystoneclient': (ksc, None), } # -- Options for openstackdocstheme ------------------------------------------- openstackdocs_repo_name = 'openstack/keystoneauth' openstackdocs_pdf_link = True openstackdocs_auto_name = False openstackdocs_bug_project = 'keystoneauth' openstackdocs_bug_tag = 'doc' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/doc/source/extras.rst0000664000175000017500000000462200000000000020457 0ustar00zuulzuul00000000000000====== Extras ====== The extensibility of keystoneauth plugins is purposefully designed to allow a range of different authentication mechanisms that don't have to reside in the upstream packages. There are however a number of plugins that upstream supports that involve additional dependencies that the keystoneauth package cannot depend upon directly. To get around this we utilize setuptools `extras dependencies `_ for additional plugins. To use a plugin like the kerberos plugin that has additional dependencies you must install the additional dependencies like:: pip install keystoneauth1[kerberos] By convention (not a requirement) extra plugins have a module located in the keystoneauth1.extras module with the same name as the dependency. eg:: from keystoneauth1.extras import kerberos There is no keystoneauth specific check that the correct dependencies are installed for accessing a module. You would expect to see standard python ImportError when the required dependencies are not found. Examples ======== All extras plugins follow the pattern: 1. import plugin module 2. instantiate the plugin 3. call get_token method of the plugin passing it a session object to get a token Kerberos -------- Get domain-scoped token using :py:class:`~keystoneauth1.extras.kerberos.Kerberos`:: from keystoneauth1.extras import kerberos from keystoneauth1 import session plugin = kerberos.Kerberos('http://example.com:5000/v3') sess = session.Session(plugin) token = plugin.get_token(sess) Get unscoped federated token:: from keystoneauth1.extras import kerberos from keystoneauth1 import session plugin = kerberos.MappedKerberos( auth_url='http://example.com:5000/v3', protocol='example_protocol', identity_provider='example_identity_provider') sess = session.Session() token = plugin.get_token(sess) Get project scoped federated token:: from keystoneauth1.extras import kerberos from keystoneauth1 import session plugin = kerberos.MappedKerberos( auth_url='http://example.com:5000/v3', protocol='example_protocol', identity_provider='example_identity_provider', project_id='example_project_id') sess = session.Session() token = plugin.get_token(sess) project_id = plugin.get_project_id(sess) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2727942 keystoneauth1-4.4.0/doc/source/images/0000775000175000017500000000000000000000000017660 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/doc/source/images/graphs_authComp.svg0000664000175000017500000000570600000000000023535 0ustar00zuulzuul00000000000000 AuthComp AuthComp Auth Component AuthComp->Reject Reject Unauthenticated Requests Service OpenStack Service AuthComp->Service Forward Authenticated Requests Start->AuthComp ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/doc/source/images/graphs_authCompDelegate.svg0000664000175000017500000000702000000000000025157 0ustar00zuulzuul00000000000000 AuthCompDelegate AuthComp Auth Component AuthComp->Reject Reject Requests Indicated by the Service Service OpenStack Service AuthComp->Service Forward Requests with Identiy Status Service->AuthComp Send Response OR Reject Message Start->AuthComp ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/doc/source/index.rst0000664000175000017500000000214500000000000020256 0ustar00zuulzuul00000000000000Common Authentication Library for OpenStack Clients =================================================== Keystoneauth provides a standard way to do authentication and service requests within the OpenStack ecosystem. It is designed for use in conjunction with the existing OpenStack clients and for simplifying the process of writing new clients. Contents -------- .. toctree:: :maxdepth: 1 using-sessions authentication-plugins plugin-options extras migrating api/modules Release Notes ------------- `Release Notes`_ .. _Release Notes: https://docs.openstack.org/releasenotes/keystoneauth/ Contributing ------------ Code is hosted `on opendev.org`_. Submit bugs to the Keystone project on `Launchpad`_. Submit code to the ``openstack/keystoneauth`` project using `Gerrit`_. .. _on opendev.org: https://opendev.org/openstack/keystoneauth .. _Launchpad: https://launchpad.net/keystoneauth .. _Gerrit: https://docs.openstack.org/infra/manual/developers.html#development-workflow Run tests with ``tox``. Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/doc/source/migrating.rst0000664000175000017500000000727100000000000021135 0ustar00zuulzuul00000000000000============================= Migrating from keystoneclient ============================= When keystoneauth was extracted from keystoneclient the basic usage of the session, adapter and auth plugins purposefully did not change. If you are using them in a supported fashion from keystoneclient then the transition should be fairly simple. Authentication Plugins ====================== The authentication plugins themselves changed very little however there were changes to the way plugins are loaded and some of the supporting classes. Plugin Loading -------------- In keystoneclient auth plugin loading is managed by the class itself. This method proved useful in allowing the plugin to control the way it was loaded however it linked the authentication logic with the config and CLI loading. In keystoneauth this has been severed and the auth plugin is handled separately from the mechanism that loads it. Authentication plugins still implement the base authentication class :py:class:`~keystoneauth1.plugin.BaseAuthPlugin`. To make the plugins capable of being loaded from CLI or CONF file you should implement the base :py:class:`~keystoneauth1.loading.BaseLoader` class which is loaded when `--os-auth-type` is used. This class handles the options that are presented, and then constructs the authentication plugin for use by the application. Largely the options that are returned will be the same as what was used in keystoneclient however in keystoneclient the options used :py:class:`oslo_config.cfg.Opt` objects. Due to trying to keep minimal dependencies there is no direct dependency from keystoneauth on oslo.config and instead options should be specified as :py:class:`~keystoneauth1.loading.Opt` objects. To ensure distinction between the plugins, the setuptools entrypoints that plugins register at has been updated to reflect keystoneauth1 and should now be: keystoneauth1.plugin AccessInfo Objects ------------------ AccessInfo objects are a representation of the information stored within a token. In keystoneclient these objects were dictionaries of the token data with property accessors. In keystoneauth the dictionary interface has been removed and just the property accessors are available. The creation function has also changed. The :py:meth:`keystoneclient.access.AccessInfo.factory` method has been removed and replaced with the :py:func:`keystoneauth1.access.create`. Step-by-step migration example ------------------------------ Add ``keystoneauth1`` to requirements.txt In the code do the following change:: -from keystoneclient import auth +from keystoneauth1 import plugin consequently:: -auth.BaseAuthPlugin +plugin.BaseAuthPlugin To import service catalog:: -from keystoneclient import service_catalog +from keystoneauth1.access import service_catalog To get url using service catalog *endpoint_type* parameter was changed to *interface*:: -service_catalog.ServiceCatalogV2(sc).service_catalog.url_for(..., endpoint_type=interface) +service_catalog.ServiceCatalogV2(sc).service_catalog.url_for(..., interface=interface) Obtaining the session:: -from keystoneclient import session +from keystoneauth1 import loading as ks_loading -_SESSION = session.Session.load_from_conf_options( -auth_plugin = auth.load_from_conf_options(conf, NEUTRON_GROUP) +_SESSION = ks_loading.load_session_from_conf_options( +auth_plugin = ks_loading.load_auth_from_conf_options(conf, NEUTRON_GROUP) Mocking session for test purposes:: -@mock.patch('keystoneclient.session.Session') +@mock.patch('keystoneauth1.session.Session') Token fixture imports haven't change much:: -from keystoneclient.fixture import V2Token +from keystoneauth1.fixture import V2Token ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/doc/source/plugin-options.rst0000664000175000017500000000540000000000000022133 0ustar00zuulzuul00000000000000============== Plugin Options ============== Using plugins via config file ----------------------------- When using the plugins via config file you define the plugin name as ``auth_type``. The options of the plugin are then specified while replacing ``-`` with ``_`` to be valid in configuration. For example to use the password_ plugin in a config file you would specify: .. code-block:: ini [section] auth_url = http://keystone.example.com:5000/ auth_type = password username = myuser password = mypassword project_name = myproject default_domain_name = mydomain Using plugins via CLI --------------------- When using auth plugins via CLI via ``os-client-config`` or ``shade`` you can specify parameters via environment configuration by using the pattern ``OS_`` followed by the uppercase parameter name replacing ``-`` with ``_``. For example to use the password_ plugin via environment variable you specify: .. code-block:: bash export OS_AUTH_TYPE=password export OS_AUTH_URL=http://keystone.example.com:5000/ export OS_USERNAME=myuser export OS_PASSWORD=mypassword export OS_PROJECT_NAME=myproject export OS_DEFAULT_DOMAIN_NAME=mydomain Specifying operations via CLI parameter will override the environment parameter. These are specified with the pattern ``--os-`` and the parameter name. Using the password_ example again: .. code-block:: bash openstack --os-auth-type password \ --os-auth-url http://keystone.example.com:5000/ \ --os-username myuser \ --os-password mypassword \ --os-project-name myproject \ --os-default-domain-name mydomain \ operation Additional loaders ------------------ The configuration and CLI loaders are quite commonly used however similar concepts are found in other situations such as ``os-client-config`` in which you specify authentication and other cloud parameters in a ``clouds.yaml`` file. Loaders such as these use the same plugin options listed below, but via their own mechanism. In ``os-client-config`` the password_ plugin looks like: .. code-block:: yaml clouds: mycloud: auth_type: password auth: auth_url: http://keystone.example.com:5000/ auth_type: password username: myuser password: mypassword project_name: myproject default_domain_name: mydomain However different services may implement loaders in their own way and you should consult their relevant documentation. The same auth options will be available. Available Plugins ----------------- This is a listing of all included plugins and the options that they accept. Plugins are listed alphabetically and not in any order of priority. .. list-auth-plugins:: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/doc/source/using-sessions.rst0000664000175000017500000006233700000000000022151 0ustar00zuulzuul00000000000000============== Using Sessions ============== Introduction ============ The :py:class:`keystoneauth1.session.Session` class was introduced into keystoneauth1 as an attempt to bring a unified interface to the various OpenStack clients that share common authentication and request parameters between a variety of services. The model for using a Session and auth plugin as well as the general terms used have been heavily inspired by the `requests `_ library. However neither the Session class nor any of the authentication plugins rely directly on those concepts from the requests library so you should not expect a direct translation. Features -------- - Common client authentication Authentication is handled by one of a variety of authentication plugins and then this authentication information is shared between all the services that use the same Session object. - Security maintenance Security code is maintained in a single place and reused between all clients such that in the event of problems it can be fixed in a single location. - Standard service and version discovery Clients are not expected to have any knowledge of an identity token or any other form of identification credential. Service, endpoint, major version discovery, and microversion support discovery are handled by the Session and plugins. Discovery information is automatically cached in memory, so the user need not worry about excessive use of discovery metadata. - Safe logging of HTTP interactions Clients need to be able to enable logging of the HTTP interactions, but some things, such as the token or secrets, need to be ommitted. Sessions for Users ================== The Session object is the contact point to your OpenStack cloud services. It stores the authentication credentials and connection information required to communicate with OpenStack such that it can be reused to communicate with many services. When creating services this Session object is passed to the client so that it may use this information. A Session will authenticate on demand. When a request that requires authentication passes through the Session the authentication plugin will be asked for a valid token. If a valid token is available it will be used otherwise the authentication plugin may attempt to contact the authentication service and fetch a new one. An example using keystoneclient to wrap a Session:: >>> from keystoneauth1.identity import v3 >>> from keystoneauth1 import session >>> from keystoneclient.v3 import client >>> auth = v3.Password(auth_url='https://my.keystone.com:5000/v3', ... username='myuser', ... password='mypassword', ... project_name='proj', ... user_domain_id='default', ... project_domain_id='default') >>> sess = session.Session(auth=auth, ... verify='/path/to/ca.cert') >>> ks = client.Client(session=sess) >>> users = ks.users.list() As other OpenStack client libraries adopt this means of operating they will be created in a similar fashion by passing the Session object to the client's constructor. Sharing Authentication Plugins ------------------------------ A Session can only contain one authentication plugin. However, there is nothing that specifically binds the authentication plugin to that Session - a new Session can be created that reuses the existing authentication plugin:: >>> new_sess = session.Session(auth=sess.auth, verify='/path/to/different-cas.cert') In this case we cannot know which Session object will be used when the plugin performs the authentication call so the command must be able to succeed with either. Authentication plugins can also be provided on a per-request basis. This will be beneficial in a situation where a single Session is juggling multiple authentication credentials:: >>> sess.get('https://my.keystone.com:5000/v3', auth=my_auth_plugin) If an auth plugin is provided via parameter then it will override any auth plugin on the Session. Sessions for Client Developers ============================== Sessions are intended to take away much of the hassle of dealing with authentication data and token formats. Clients should be able to specify filter parameters for selecting the endpoint and have the parsing of the catalog managed for them. Major Version Discovery and Microversion Support ------------------------------------------------ In OpenStack, the root URLs of available services are distributed to the user in an object called the Service Catalog, which is part of the token they receive. Clients are expected to use the URLs from the Service Catalog rather than have them provided. The root URL of a given service is referred to as the `endpoint` of the service. The URL of a specific version of a service is referred to as a `versioned endpoint`. REST requests for a service are made against a given `versioned endpoint`. The topic of Major API versions and microversions can be confusing. As `keystoneauth` provides facilities for discovery of versioned endpoints associated with a Major API Version and for fetching information about the microversions that versioned endpoint supports, it is important to be aware of the distinction between the two. Conceptually the most important thing to understand is that a Major API Version describes the URL of a discrete versioned endpoint, while a given versioned endpoint might have properties that express that it supports a range of microversions. When a user wants to make a REST request against a service, the user expresses the Major API version and the type of service so that the appropriate versioned endpoint can be found and used. For example, a user might request version 2 of the compute service from cloud.example.com and end up with a versioned endpoint of ``https://compute.example.com/v2``. Each service provides a discovery document at the root of each versioned endpoint that contains information about that versioned endpoint. Each service also provides a document at the root of the unversioned endpoint that contains a list of the discovery documents for all of the available versioned endpoints. By examining these documents, it is possible to find the versioned endpoint that corresponds with the user's desired Major API version. Each of those documents may also indicate that the given versioned endpoint supports microversions by listing a minimum and maximum microversion that it understands. As a result of having found the versioned endpoint for the requested Major API version, the user will also know which microversions, if any, may be used in requests to that versioned endpoint. When a client makes REST requests to the Major API version's endpoint, the client can, optionally, on a request-by-request basis, include a header specifying that the individual request use the behavior defined by the given microversion. If a client does not request a microversion, the service will behave as if the minimum supported microversion was specified. .. note: The changes that each microversion reflects are documented elsewhere and are not information provided by the discovery process. The overall transaction then has three parts: * What is the endpoint for a given Major API version of a given service? * What are the minimum and maximum microversions supported at that endpoint? * Which one of that range of microversions, if any, does the user want to use for a given request? `keystoneauth` provides facilities for discovering the endpoint for a given Major API of a given service, as well as reporting the available microversion ranges that endpoint supports, if any. More information is available in the `API-WG Specs`_ on the topics of `Microversions`_ and `Consuming the Catalog`_. Authentication -------------- When making a request with a Session object you can simply pass the keyword parameter ``authenticated`` to indicate whether the argument should contain a token, by default a token is included if an authentication plugin is available:: >>> # In keystone this route is unprotected by default >>> resp = sess.get('https://my.keystone.com:5000/v3', authenticated=False) Service Discovery ----------------- In general a client does not need to know the full URL for the server that they are communicating with, simply that it should send a request to a path belonging to the correct service. This is controlled by the ``endpoint_filter`` parameter to a request which contains all the information an authentication plugin requires to determine the correct URL to which to send a request. When using this mode only the path for the request needs to be specified:: >>> resp = session.get('/users', endpoint_filter={'service_type': 'identity', 'interface': 'admin', 'region_name': 'myregion', 'min_version': '2.0', 'max_version': '3.4', 'discover_versions': False}) .. note:: The min_version and max_version arguments in this example indicate acceptable range for finding the endpoint for the given Major API versions. They are in the endpoint_filter, they are not requesting the call to ``/users`` be made at a specific microversion. `endpoint_filter` accepts a number of arguments with which it can determine an endpoint url: service_type the type of service. For example ``identity``, ``compute``, ``volume`` or many other predefined identifiers. interface the network exposure the interface has. Can also be a list, in which case the first matching interface will be used. Valid values are: - ``public``: An endpoint that is available to the wider internet or network. - ``internal``: An endpoint that is only accessible within the private network. - ``admin``: An endpoint to be used for administrative tasks. region_name the name of the region where the endpoint resides. version the minimum version, restricted to a given Major API. For instance, a `version` of ``2.2`` will match ``2.2`` and ``2.3`` but not ``2.1`` or ``3.0``. Mutually exclusive with `min_version` and `max_version`. min_version the minimum version of a given API, intended to be used as the lower bound of a range with `max_version`. See `max_version` for examples. Mutually exclusive with `version`. max_version the maximum version of a given API, intended to be used as the upper bound of a range with `min_version`. For example:: 'min_version': '2.2', 'max_version': '3.3' will match ``2.2``, ``2.10``, ``3.0``, and ``3.3``, but not ``1.42``, ``2.1``, or ``3.20``. Mutually exclusive with `version`. .. note:: version, min_version and max_version are all used to help determine the endpoint for a given Major API version of a service. discover_versions whether or not version discovery should be run, even if not strictly necessary. It is often possible to fulfill an endpoint request purely from the catalog, meaning the version discovery API is a potentially wasted additional call. However, it's possible that running discovery instead of inference is desired. Defaults to ``True``. All version arguments (`version`, `min_version` and `max_version`) can be given as: * string: ``'2.0'`` * int: ``2`` * float: ``2.0`` * tuple of ints: ``(2, 0)`` `version` and `max_version` can also be given the string ``latest``, which indicates that the highest available version should be used. The endpoint filter is a simple key-value filter and can be provided with any number of arguments. It is then up to the auth plugin to correctly use the parameters it understands. If you want to further limit your service discovery by allowing experimental APIs or disallowing deprecated APIs, you can use the ``allow`` parameter:: >>> resp = session.get('//volumes', endpoint_filter={'service_type': 'volume', 'interface': 'public', 'version': 1}, allow={'allow_deprecated': False}) The discoverable types of endpoints that `allow` can recognize are: - `allow_deprecated`: Allow deprecated version endpoints. - `allow_experimental`: Allow experimental version endpoints. - `allow_unknown`: Allow endpoints with an unrecognised status. The Session object creates a valid request by determining the URL matching the filters and appending it to the provided path. If multiple URL matches are found then any one may be chosen. While authentication plugins will endeavour to maintain a consistent set of arguments for an ``endpoint_filter`` the concept of an authentication plugin is purposefully generic. A specific mechanism may not know how to interpret certain arguments in which case it may ignore them. For example the :class:`keystoneauth1.token_endpoint.Token` plugin (which is used when you want to always use a specific endpoint and token combination) will always return the same endpoint regardless of the parameters to ``endpoint_filter`` or a custom OpenStack authentication mechanism may not have the concept of multiple ``interface`` options and choose to ignore that parameter. There is some expectation on the user that they understand the limitations of the authentication system they are using. Using Adapters -------------- If the developer would prefer not to provide `endpoint_filter` with every API call, a :class:`keystoneauth1.adapter.Adapter` can be created. The `Adapter` constructor takes the same arguments as `endpoint_filter`, as well as a `Session`. An `Adapter` behaves much like a `Session`, with the same REST methods, but is "mounted" on the endpoint that would be found by `endpoint_filter`. .. code-block:: python adapter = keystoneauth1.adapter.Adapter( session=session, service_type='volume', interface='public', version=1) response = adapter.get('/volumes') As with ``endpoint_filter`` on a Session, the ``version``, ``min_version`` and ``max_version`` parameters exist to help determine the appropriate endpoint for a Major API of a service. Endpoint Metadata ----------------- Both :class:`keystoneauth1.adapter.Adapter` and :class:`keystoneauth1.session.Session` have a method for getting metadata about the endpoint found for a given service: ``get_endpoint_data``. On the :class:`keystoneauth1.session.Session` it takes the same arguments as `endpoint_filter`. On the :class:`keystoneauth1.adapter.Adapter` it does not take arguments, as it returns the information for the Endpoint the Adapter is mounted on. ``get_endpoint_data`` returns an :class:`keystoneauth1.discovery.EndpointData` object. This object can be used to find information about the Endpoint, including which major `api_version` was found, or which `interface` in case of ranges, lists of input values or ``latest`` version. It can also be used to determine the `min_microversion` and `max_microversion` supported by the API. If an API does not support microversions, the values for both will be ``None``. It will also contain values for `next_min_version` and `not_before` if they exist for the endpoint, or ``None`` if they do not. The :class:`keystoneauth1.discovery.EndpointData` object will always contain microversion related attributes regardless of whether the REST document does or not. ``get_endpoint_data`` makes use of the same cache as the rest of the discovery process, so calling it should incur no undue expense. By default it will make at least one version discovery call so that it can fetch microversion metadata. If the user knows a service does not support microversions and is merely curious as to which major version was discovered, ``discover_versions`` can be set to ``False`` to prevent fetching microversion metadata. Requesting a Microversion ------------------------- A user who wants to specify a microversion for a given request can pass it to the ``microversion`` parameter of the `request` method on the :class:`keystoneauth1.session.Session` object, or the :class:`keystoneauth1.adapter.Adapter` object. This will cause `keystoneauth` to pass the appropriate header to the service informing the service of the microversion the user wants. .. code-block:: python resp = session.get('/volumes', microversion='3.15', endpoint_filter={'service_type': 'volume', 'interface': 'public', 'min_version': '3', 'max_version': 'latest'}) If the user is using a :class:`keystoneauth1.adapter.Adapter`, the `service_type`, which is a part of the data sent in the microversion header, will be taken from the Adapter's `service_type`. .. code-block:: python adapter = keystoneauth1.adapter.Adapter( session=session, service_type='compute', interface='public', min_version='2.1') response = adapter.get('/servers', microversion='2.38') The user can also provide a ``default_microversion`` parameter to the Adapter constructor which will be used on all requests where an explicit microversion is not requested. .. code-block:: python adapter = keystoneauth1.adapter.Adapter( session=session, service_type='compute', interface='public', min_version='2.1', default_microversion='2.38') response = adapter.get('/servers') If the user is using a :class:`keystoneauth1.session.Session`, the `service_type` will be taken from the `service_type` in `endpoint_filter`. If the `service_type` is the incorrect value to use for the microversion header for the service in question, the parameter `microversion_service_type` can be given. For instance, although keystoneauth already knows about Cinder, the `service_type` for Cinder is ``block-storage`` but the microversion header expects ``volume``. .. code-block:: python # Interactions with cinder do not need to explicitly override the # microversion_service_type - it is only being used as an example for the # use of the parameter. resp = session.get('/volumes', microversion='3.15', microversion_service_type='volume', endpoint_filter={'service_type': 'block-storage', 'interface': 'public', 'min_version': '3', 'max_version': 'latest'}) Logging ======= The logging system uses standard `python logging`_ rooted on the ``keystoneauth`` namespace as would be expected. There are two possibilities of where log messages about HTTP interactions will go. By default, all messages will go to the ``keystoneauth.session`` logger. If the ``split_loggers`` option on the :class:`keystoneauth1.session.Session` constructor is set to ``True``, the HTTP content will be split across four subloggers to allow for fine-grained control of what is logged and how: keystoneauth.session.request-id Emits a log entry at the ``DEBUG`` level for every http request including information about the URL, ``service-type`` and ``request-id``. keystoneauth.session.request Emits a log entry at the ``DEBUG`` level for every http request including a curl formatted string of the request. keystoneauth.session.response Emits a log entry at the ``DEBUG`` level for every http response received, including the status code, and the headers received. keystoneauth.session.body Emits a log entry at the ``DEBUG`` level containing the contents of the response body if the ``content-type`` is either ``text`` or ``json``. Using loggers ------------- A full description of how to consume `python logging`_ is out of scope of this document, but a few simple examples are provided. If you would like to configure logging to log keystoneuath at the ``INFO`` level with no ``DEBUG`` messages: .. code-block:: python import keystoneauth1 import logging logger = logging.getLogger('keystoneauth') logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.INFO) If you would like to get a full HTTP debug trace including bodies: .. code-block:: python import keystoneauth1 import logging logger = logging.getLogger('keystoneauth') logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.DEBUG) If you would like to get a full HTTP debug trace bug with no bodies: .. code-block:: python import keystoneauth1 import keystoneauth1.session import logging logger = logging.getLogger('keystoneauth') logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.DEBUG) body_logger = logging.getLogger('keystoneauth.session.body') body_logger.setLevel(logging.WARN) session = keystoneauth1.session.Session(split_loggers=True) Finally, if you would like to log request-ids and response headers to one file, request commands, response headers and response bodies to a different file, and everything else to the console: .. code-block:: python import keystoneauth1 import keystoneauth1.session import logging # Create a handler that outputs only outputs INFO level messages to stdout stream_handler = logging.StreamHandler() stream_handler.setLevel(logging.INFO) # Configure the default behavior of all keystoneauth logging to log at the # INFO level. logger = logging.getLogger('keystoneauth') logger.setLevel(logging.INFO) # Emit INFO messages from all keystoneauth loggers to stdout logger.addHandler(stream_handler) # Create an output formatter that includes logger name and timestamp. formatter = logging.Formatter('%(asctime)s %(name)s %(message)s') # Create a file output for request ids and response headers request_handler = logging.FileHandler('request.log') request_handler.setFormatter(formatter) # Create a file output for request commands, response headers and bodies body_handler = logging.FileHandler('response-body.log') body_handler.setFormatter(formatter) # Log all HTTP interactions at the DEBUG level session_logger = logging.getLogger('keystoneauth.session') session_logger.setLevel(logging.DEBUG) # Emit request ids to the request log request_id_logger = logging.getLogger('keystoneauth.session.request-id') request_id_logger.addHandler(request_handler) # Emit response headers to both the request log and the body log header_logger = logging.getLogger('keystoneauth.session.response') header_logger.addHandler(request_handler) header_logger.addHandler(body_handler) # Emit request commands to the body log request_logger = logging.getLogger('keystoneauth.session.request') request_logger.addHandler(body_handler) # Emit bodies only to the body log body_logger = logging.getLogger('keystoneauth.session.body') body_logger.addHandler(body_handler) session = keystoneauth1.session.Session(split_loggers=True) The above will produce messages like the following in request.log: :: 2017-09-19 22:10:09,466 keystoneauth.session.request-id GET call to volumev2 for http://cloud.example.com/volume/v2/137155c35fb34172a284a3c2540c92ab/volumes/detail used request id req-f4f2058a-9308-4c4a-94e6-5ee1cd6c78bd 2017-09-19 22:10:09,751 keystoneauth.session.response [200] Date: Tue, 19 Sep 2017 22:10:09 GMT Server: Apache/2.4.18 (Ubuntu) x-compute-request-id: req-2e9181d2-9f3e-404e-a12f-1f1566736ab3 Content-Type: application/json Content-Length: 15 x-openstack-request-id: req-2e9181d2-9f3e-404e-a12f-1f1566736ab3 Connection: close And content like the following into response-body.log: :: 2017-09-19 22:10:09,490 keystoneauth.session.request curl -g -i -X GET http://cloud.example.com/volume/v2/137155c35fb34172a284a3c2540c92ab/volumes/detail?marker=34cd00cf-bf67-4667-a900-5ce233e383d5 -H "User-Agent: os-client-config/1.28.0 shade/1.23.1 keystoneauth1/3.2.0 python-requests/2.18.4 CPython/2.7.12" -H "X-Auth-Token: {SHA1}a1d03d2a4cbee590a55f1786d452e1027d5fd781" 2017-09-19 22:10:09,751 keystoneauth.session.response [200] Date: Tue, 19 Sep 2017 22:10:09 GMT Server: Apache/2.4.18 (Ubuntu) x-compute-request-id: req-2e9181d2-9f3e-404e-a12f-1f1566736ab3 Content-Type: application/json Content-Length: 15 x-openstack-request-id: req-2e9181d2-9f3e-404e-a12f-1f1566736ab3 Connection: close 2017-09-19 22:10:09,751 keystoneauth.session.body {"volumes": []} User Provided Loggers --------------------- The HTTP methods (request, get, post, put, etc) on `keystoneauth1.session.Session` and `keystoneauth1.adapter.Adapter` all support a ``logger`` parameter. A user can provide their own `logger`_ which will override the session loggers mentioned above. If a single logger is provided in this manner, request, response and body content will all be logged to that logger at the ``DEBUG`` level, and the strings ``REQ:``, ``RESP:`` and ``RESP BODY:`` will be pre-pended as appropriate. .. _API-WG Specs: https://specs.openstack.org/openstack/api-wg/ .. _Consuming the Catalog: https://specs.openstack.org/openstack/api-wg/guidelines/consuming-catalog.html .. _Microversions: https://specs.openstack.org/openstack/api-wg/guidelines/microversion_specification.html#version-discovery .. _python logging: https://docs.python.org/3/library/logging.html .. _logger: https://docs.python.org/3/library/logging.html#logging.Logger ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2767942 keystoneauth1-4.4.0/keystoneauth1/0000775000175000017500000000000000000000000017152 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/__init__.py0000664000175000017500000000117700000000000021271 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import pbr.version __version__ = pbr.version.VersionInfo('keystoneauth1').version_string() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/_fair_semaphore.py0000664000175000017500000000741700000000000022660 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import threading import time from six.moves import queue class FairSemaphore(object): """Semaphore class that notifies in order of request. We cannot use a normal Semaphore because it doesn't give any ordering, which could lead to a request starving. Instead, handle them in the order we receive them. :param int concurrency: How many concurrent threads can have the semaphore at once. :param float rate_delay: How long to wait between the start of each thread receiving the semaphore. """ def __init__(self, concurrency, rate_delay): self._lock = threading.Lock() self._concurrency = concurrency if concurrency: self._count = 0 self._queue = queue.Queue() self._rate_delay = rate_delay self._rate_last_ts = time.time() def __enter__(self): """Aquire a semaphore.""" # If concurrency is None, everyone is free to immediately execute. if not self._concurrency: # NOTE: Rate limiting still applies.This will ultimately impact # concurrency a bit due to the mutex. with self._lock: execution_time = self._advance_timer() else: execution_time = self._get_ticket() return self._wait_for_execution(execution_time) def _wait_for_execution(self, execution_time): """Wait until the pre-calculated time to run.""" wait_time = execution_time - time.time() if wait_time > 0: time.sleep(wait_time) def _get_ticket(self): ticket = threading.Event() with self._lock: if self._count <= self._concurrency: # We can execute, no need to wait. Take a ticket and # move on. self._count += 1 return self._advance_timer() else: # We need to wait for a ticket before we can execute. # Put ourselves in the ticket queue to be woken up # when available. self._queue.put(ticket) ticket.wait() with self._lock: return self._advance_timer() def _advance_timer(self): """Calculate the time when it's ok to run a command again. This runs inside of the mutex, serializing the calculation of when it's ok to run again and setting _rate_last_ts to that new time so that the next thread to calculate when it's safe to run starts from the time that the current thread calculated. """ self._rate_last_ts = self._rate_last_ts + self._rate_delay return self._rate_last_ts def __exit__(self, exc_type, exc_value, traceback): """Release the semaphore.""" # If concurrency is None, everyone is free to immediately execute if not self._concurrency: return with self._lock: # If waiters, wake up the next item in the queue (note # we're under the queue lock so the queue won't change # under us). if self._queue.qsize() > 0: ticket = self._queue.get() ticket.set() else: # Nothing else to do, give our ticket back self._count -= 1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/_utils.py0000664000175000017500000000461400000000000021030 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime import logging import iso8601 import six def get_logger(name): name = name.replace(__name__.split('.')[0], 'keystoneauth') return logging.getLogger(name) logger = get_logger(__name__) def normalize_time(timestamp): """Normalize time in arbitrary timezone to UTC naive object.""" offset = timestamp.utcoffset() if offset is None: return timestamp return timestamp.replace(tzinfo=None) - offset def parse_isotime(timestr): """Parse time from ISO 8601 format.""" try: return iso8601.parse_date(timestr) except iso8601.ParseError as e: raise ValueError(six.text_type(e)) except TypeError as e: raise ValueError(six.text_type(e)) def from_utcnow(**timedelta_kwargs): r"""Calculate the time in the future from utcnow. :param \*\*timedelta_kwargs: Passed directly to :class:`datetime.timedelta` to add to the current time in UTC. :returns: The time in the future based on ``timedelta_kwargs``. :rtype: datetime.datetime """ now = datetime.datetime.utcnow() delta = datetime.timedelta(**timedelta_kwargs) return now + delta def before_utcnow(**timedelta_kwargs): r"""Calculate the time in the past from utcnow. :param \*\*timedelta_kwargs: Passed directly to :class:`datetime.timedelta` to subtract from the current time in UTC. :returns: The time in the past based on ``timedelta_kwargs``. :rtype: datetime.datetime """ now = datetime.datetime.utcnow() delta = datetime.timedelta(**timedelta_kwargs) return now - delta # Detect if running on the Windows Subsystem for Linux try: with open('/proc/version', 'r') as f: is_windows_linux_subsystem = 'microsoft' in f.read().lower() except IOError: is_windows_linux_subsystem = False ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2807944 keystoneauth1-4.4.0/keystoneauth1/access/0000775000175000017500000000000000000000000020413 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/access/__init__.py0000664000175000017500000000136200000000000022526 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.access.access import * # noqa __all__ = ('AccessInfo', # noqa: F405 'AccessInfoV2', # noqa: F405 'AccessInfoV3', # noqa: F405 'create') # noqa: F405 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/access/access.py0000664000175000017500000004664000000000000022240 0ustar00zuulzuul00000000000000# Copyright 2012 Nebula, Inc. # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import functools from keystoneauth1 import _utils as utils from keystoneauth1.access import service_catalog from keystoneauth1.access import service_providers # gap, in seconds, to determine whether the given token is about to expire STALE_TOKEN_DURATION = 30 __all__ = ('AccessInfo', 'AccessInfoV2', 'AccessInfoV3', 'create') def create(resp=None, body=None, auth_token=None): if resp and not body: body = resp.json() if 'token' in body: if resp and not auth_token: auth_token = resp.headers.get('X-Subject-Token') return AccessInfoV3(body, auth_token) elif 'access' in body: return AccessInfoV2(body, auth_token) raise ValueError('Unrecognized auth response') def _missingproperty(f): @functools.wraps(f) def inner(self): try: return f(self) except KeyError: return None return property(inner) class AccessInfo(object): """Encapsulates a raw authentication token from keystone. Provides helper methods for extracting useful values from that token. """ _service_catalog_class = None def __init__(self, body, auth_token=None): self._data = body self._auth_token = auth_token self._service_catalog = None self._service_providers = None @property def service_catalog(self): if not self._service_catalog: self._service_catalog = self._service_catalog_class.from_token( self._data) return self._service_catalog def will_expire_soon(self, stale_duration=STALE_TOKEN_DURATION): """Determine if expiration is about to occur. :returns: true if expiration is within the given duration :rtype: boolean """ norm_expires = utils.normalize_time(self.expires) # (gyee) should we move auth_token.will_expire_soon() to timeutils # instead of duplicating code here? soon = utils.from_utcnow(seconds=stale_duration) return norm_expires < soon def has_service_catalog(self): """Return true if the auth token has a service catalog. :returns: boolean """ raise NotImplementedError() @property def auth_token(self): """Return the token_id associated with the auth request. To be used in headers for authenticating OpenStack API requests. :returns: str """ return self._auth_token @property def expires(self): """Return the token expiration (as datetime object). :returns: datetime """ raise NotImplementedError() @property def issued(self): """Return the token issue time (as datetime object). :returns: datetime """ raise NotImplementedError() @property def username(self): """Return the username associated with the auth request. Follows the pattern defined in the V2 API of first looking for 'name', returning that if available, and falling back to 'username' if name is unavailable. :returns: str """ raise NotImplementedError() @property def user_id(self): """Return the user id associated with the auth request. :returns: str """ raise NotImplementedError() @property def user_domain_id(self): """Return the user's domain id associated with the auth request. :returns: str """ raise NotImplementedError() @property def user_domain_name(self): """Return the user's domain name associated with the auth request. :returns: str """ raise NotImplementedError() @property def role_ids(self): """Return a list of user's role ids associated with the auth request. :returns: a list of strings of role ids """ raise NotImplementedError() @property def role_names(self): """Return a list of user's role names associated with the auth request. :returns: a list of strings of role names """ raise NotImplementedError() @property def domain_name(self): """Return the domain name associated with the auth request. :returns: str or None (if no domain associated with the token) """ raise NotImplementedError() @property def domain_id(self): """Return the domain id associated with the auth request. :returns: str or None (if no domain associated with the token) """ raise NotImplementedError() @property def project_name(self): """Return the project name associated with the auth request. :returns: str or None (if no project associated with the token) """ raise NotImplementedError() @property def tenant_name(self): """Synonym for project_name.""" return self.project_name @property def scoped(self): """Return true if the auth token was scoped. Returns true if scoped to a tenant(project) or domain, and contains a populated service catalog. This is deprecated, use project_scoped instead. :returns: bool """ return self.project_scoped or self.domain_scoped or self.system_scoped @property def project_scoped(self): """Return true if the auth token was scoped to a tenant (project). :returns: bool """ return bool(self.project_id) @property def domain_scoped(self): """Return true if the auth token was scoped to a domain. :returns: bool """ raise NotImplementedError() @property def system_scoped(self): """Return true if the auth token was scoped to the system. :returns: bool """ raise NotImplementedError() @property def trust_id(self): """Return the trust id associated with the auth request. :returns: str or None (if no trust associated with the token) """ raise NotImplementedError() @property def trust_scoped(self): """Return true if the auth token was scoped from a delegated trust. The trust delegation is via the OS-TRUST v3 extension. :returns: bool """ raise NotImplementedError() @property def trustee_user_id(self): """Return the trustee user id associated with a trust. :returns: str or None (if no trust associated with the token) """ raise NotImplementedError() @property def trustor_user_id(self): """Return the trustor user id associated with a trust. :returns: str or None (if no trust associated with the token) """ raise NotImplementedError() @property def project_id(self): """Return the project ID associated with the auth request. This returns None if the auth token wasn't scoped to a project. :returns: str or None (if no project associated with the token) """ raise NotImplementedError() @property def tenant_id(self): """Synonym for project_id.""" return self.project_id @property def project_domain_id(self): """Return the project's domain id associated with the auth request. :returns: str """ raise NotImplementedError() @property def project_domain_name(self): """Return the project's domain name associated with the auth request. :returns: str """ raise NotImplementedError() @property def oauth_access_token_id(self): """Return the access token ID if OAuth authentication used. :returns: str or None. """ raise NotImplementedError() @property def oauth_consumer_id(self): """Return the consumer ID if OAuth authentication used. :returns: str or None. """ raise NotImplementedError() @property def is_federated(self): """Return true if federation was used to get the token. :returns: boolean """ raise NotImplementedError() @property def is_admin_project(self): """Return true if the current project scope is the admin project. For backwards compatibility purposes if there is nothing specified in the token we always assume we are in the admin project, so this will default to True. :returns boolean """ raise NotImplementedError() @property def audit_id(self): """Return the audit ID if present. :returns: str or None. """ raise NotImplementedError() @property def audit_chain_id(self): """Return the audit chain ID if present. In the event that a token was rescoped then this ID will be the :py:attr:`audit_id` of the initial token. Returns None if no value present. :returns: str or None. """ raise NotImplementedError() @property def initial_audit_id(self): """The audit ID of the initially requested token. This is the :py:attr:`audit_chain_id` if present or the :py:attr:`audit_id`. """ return self.audit_chain_id or self.audit_id @property def service_providers(self): """Return an object representing the list of trusted service providers. Used for Keystone2Keystone federating-out. :returns: :py:class:`keystoneauth1.service_providers.ServiceProviders` or None """ raise NotImplementedError() @property def bind(self): """Information about external mechanisms the token is bound to. If a token is bound to an external authentication mechanism it can only be used in conjunction with that mechanism. For example if bound to a kerberos principal it may only be accepted if there is also kerberos authentication performed on the request. :returns: A dictionary or None. The key will be the bind type the value is a dictionary that is specific to the format of the bind type. Returns None if there is no bind information in the token. """ raise NotImplementedError() @property def project_is_domain(self): """Return if a project act as a domain. :returns: bool """ raise NotImplementedError() class AccessInfoV2(AccessInfo): """An object for encapsulating raw v2 auth token from identity service.""" version = 'v2.0' _service_catalog_class = service_catalog.ServiceCatalogV2 def has_service_catalog(self): return 'serviceCatalog' in self._data.get('access', {}) @_missingproperty def auth_token(self): set_token = super(AccessInfoV2, self).auth_token return set_token or self._data['access']['token']['id'] @property def _token(self): return self._data['access']['token'] @_missingproperty def expires(self): return utils.parse_isotime(self._token.get('expires')) @_missingproperty def issued(self): return utils.parse_isotime(self._token['issued_at']) @property def _user(self): return self._data['access']['user'] @_missingproperty def username(self): return self._user.get('name') or self._user.get('username') @_missingproperty def user_id(self): return self._user['id'] @property def user_domain_id(self): return None @property def user_domain_name(self): return None @_missingproperty def role_ids(self): metadata = self._data.get('access', {}).get('metadata', {}) return metadata.get('roles', []) @_missingproperty def role_names(self): return [r['name'] for r in self._user.get('roles', [])] @property def domain_name(self): return None @property def domain_id(self): return None @property def project_name(self): try: tenant_dict = self._token['tenant'] except KeyError: pass else: return tenant_dict.get('name') # pre grizzly try: return self._user['tenantName'] except KeyError: pass # pre diablo, keystone only provided a tenantId try: return self._token['tenantId'] except KeyError: pass @property def domain_scoped(self): return False @property def system_scoped(self): return False @property def _trust(self): return self._data['access']['trust'] @_missingproperty def trust_id(self): return self._trust['id'] @_missingproperty def trust_scoped(self): return bool(self._trust) @_missingproperty def trustee_user_id(self): return self._trust['trustee_user_id'] @property def trustor_user_id(self): # this information is not available in the v2 token bug: #1331882 return None @property def project_id(self): try: tenant_dict = self._token['tenant'] except KeyError: pass else: return tenant_dict.get('id') # pre grizzly try: return self._user['tenantId'] except KeyError: pass # pre diablo try: return self._token['tenantId'] except KeyError: pass @property def project_is_domain(self): return False @property def project_domain_id(self): return None @property def project_domain_name(self): return None @property def oauth_access_token_id(self): return None @property def oauth_consumer_id(self): return None @property def is_federated(self): return False @property def is_admin_project(self): return True @property def audit_id(self): try: return self._token.get('audit_ids', [])[0] except IndexError: return None @property def audit_chain_id(self): try: return self._token.get('audit_ids', [])[1] except IndexError: return None @property def service_providers(self): return None @_missingproperty def bind(self): return self._token['bind'] class AccessInfoV3(AccessInfo): """An object encapsulating raw v3 auth token from identity service.""" version = 'v3' _service_catalog_class = service_catalog.ServiceCatalogV3 def has_service_catalog(self): return 'catalog' in self._data['token'] @property def _user(self): return self._data['token']['user'] @property def is_federated(self): return 'OS-FEDERATION' in self._user @property def is_admin_project(self): return self._data.get('token', {}).get('is_admin_project', True) @_missingproperty def expires(self): return utils.parse_isotime(self._data['token']['expires_at']) @_missingproperty def issued(self): return utils.parse_isotime(self._data['token']['issued_at']) @_missingproperty def user_id(self): return self._user['id'] @property def user_domain_id(self): try: return self._user['domain']['id'] except KeyError: if self.is_federated: return None raise @property def user_domain_name(self): try: return self._user['domain']['name'] except KeyError: if self.is_federated: return None raise @_missingproperty def role_ids(self): return [r['id'] for r in self._data['token'].get('roles', [])] @_missingproperty def role_names(self): return [r['name'] for r in self._data['token'].get('roles', [])] @_missingproperty def username(self): return self._user['name'] @_missingproperty def system(self): return self._data['token']['system'] @property def _domain(self): return self._data['token']['domain'] @_missingproperty def domain_name(self): return self._domain['name'] @_missingproperty def domain_id(self): return self._domain['id'] @property def _project(self): return self._data['token']['project'] @_missingproperty def project_id(self): return self._project['id'] @_missingproperty def project_is_domain(self): return self._data['token']['is_domain'] @_missingproperty def project_domain_id(self): return self._project['domain']['id'] @_missingproperty def project_domain_name(self): return self._project['domain']['name'] @_missingproperty def project_name(self): return self._project['name'] @property def domain_scoped(self): try: return bool(self._domain) except KeyError: return False @_missingproperty def system_scoped(self): return self._data['token']['system'].get('all', False) @property def _trust(self): return self._data['token']['OS-TRUST:trust'] @_missingproperty def trust_id(self): return self._trust['id'] @property def trust_scoped(self): try: return bool(self._trust) except KeyError: return False @_missingproperty def trustee_user_id(self): return self._trust['trustee_user']['id'] @_missingproperty def trustor_user_id(self): return self._trust['trustor_user']['id'] @property def application_credential(self): return self._data['token']['application_credential'] @_missingproperty def application_credential_id(self): return self._data['token']['application_credential']['id'] @_missingproperty def application_credential_access_rules(self): return self._data['token']['application_credential']['access_rules'] @property def _oauth(self): return self._data['token']['OS-OAUTH1'] @_missingproperty def oauth_access_token_id(self): return self._oauth['access_token_id'] @_missingproperty def oauth_consumer_id(self): return self._oauth['consumer_id'] @_missingproperty def audit_id(self): try: return self._data['token']['audit_ids'][0] except IndexError: return None @_missingproperty def audit_chain_id(self): try: return self._data['token']['audit_ids'][1] except IndexError: return None @property def service_providers(self): if not self._service_providers: self._service_providers = ( service_providers.ServiceProviders.from_token(self._data)) return self._service_providers @_missingproperty def bind(self): return self._data['token']['bind'] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/access/service_catalog.py0000664000175000017500000005714500000000000024133 0ustar00zuulzuul00000000000000# Copyright 2011 OpenStack Foundation # Copyright 2011, Piston Cloud Computing, Inc. # Copyright 2011 Nebula, Inc. # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc import copy import six from keystoneauth1 import discover from keystoneauth1 import exceptions @six.add_metaclass(abc.ABCMeta) class ServiceCatalog(object): """Helper methods for dealing with a Keystone Service Catalog.""" def __init__(self, catalog): self._catalog = catalog def _get_endpoint_region(self, endpoint): return endpoint.get('region_id') or endpoint.get('region') @property def catalog(self): """Return the raw service catalog content, mostly useful for debugging. Applications should avoid this and use accessor methods instead. However, there are times when inspecting the raw catalog can be useful for analysis and other reasons. """ return self._catalog @abc.abstractmethod def is_interface_match(self, endpoint, interface): """Helper function to normalize endpoint matching across v2 and v3. :returns: True if the provided endpoint matches the required interface otherwise False. """ @staticmethod def normalize_interface(self, interface): """Handle differences in the way v2 and v3 catalogs specify endpoint. Both v2 and v3 must be able to handle the endpoint style of the other. For example v2 must be able to handle a 'public' interface and v3 must be able to handle a 'publicURL' interface. :returns: the endpoint string in the format appropriate for this service catalog. """ return interface def _normalize_endpoints(self, endpoints): """Translate endpoint description dicts into v3 form. Takes a raw endpoint description from the catalog and changes it to be in v3 format. It also saves a copy of the data in raw_endpoint so that it can be returned by methods that expect the actual original data. :param list endpoints: List of endpoint description dicts :returns: List of endpoint description dicts in v3 format """ new_endpoints = [] for endpoint in endpoints: raw_endpoint = endpoint.copy() new_endpoint = endpoint.copy() new_endpoint['raw_endpoint'] = raw_endpoint new_endpoints.append(new_endpoint) return new_endpoints def _denormalize_endpoints(self, endpoints): """Return original endpoint description dicts. Takes a list of EndpointData objects and returns the original dict that was returned from the catalog. :param list endpoints: List of `keystoneauth1.discover.EndpointData` :returns: List of endpoint description dicts in original catalog format """ return [endpoint.raw_endpoint for endpoint in endpoints] def normalize_catalog(self): """Return the catalog normalized into v3 format.""" catalog = [] for service in copy.deepcopy(self._catalog): if 'type' not in service: continue # NOTE(jamielennox): service_name is different. It is not available # in API < v3.3. If it is in the catalog then we enforce it, if it # is not then we don't because the name could be correct we just # don't have that information to check against. Set to None so # that checks will naturally work. service.setdefault('name', None) # NOTE(jamielennox): there is no such thing as a service_id in v2 # similarly to service_name. service.setdefault('id', None) service['endpoints'] = self._normalize_endpoints( service.get('endpoints', [])) for endpoint in service['endpoints']: endpoint['region_name'] = self._get_endpoint_region(endpoint) endpoint.setdefault('id', None) catalog.append(service) return catalog def _get_interface_list(self, interface): if not interface: return [] if not isinstance(interface, (list, tuple, set)): interface = [interface] return [self.normalize_interface(i) for i in interface] def get_endpoints_data(self, service_type=None, interface=None, region_name=None, service_name=None, service_id=None, endpoint_id=None): """Fetch and filter endpoint data for the specified service(s). Returns endpoints for the specified service (or all) containing the specified type (or all) and region (or all) and service name. If there is no name in the service catalog the service_name check will be skipped. This allows compatibility with services that existed before the name was available in the catalog. Valid interface types: `public` or `publicURL`, `internal` or `internalURL`, `admin` or 'adminURL` :param string service_type: Service type of the endpoint. :param interface: Type of endpoint. Can be a single value or a list of values. If it's a list of values, they will be looked for in order of preference. :param string region_name: Region of the endpoint. :param string service_name: The assigned name of the service. :param string service_id: The identifier of a service. :param string endpoint_id: The identifier of an endpoint. :returns: a list of matching EndpointData objects :rtype: list(`keystoneauth1.discover.EndpointData`) :returns: a dict, keyed by service_type, of lists of EndpointData """ interfaces = self._get_interface_list(interface) matching_endpoints = {} for service in self.normalize_catalog(): if service_type and not discover._SERVICE_TYPES.is_match( service_type, service['type']): continue if (service_name and service['name'] and service_name != service['name']): continue if (service_id and service['id'] and service_id != service['id']): continue matching_endpoints.setdefault(service['type'], []) for endpoint in service.get('endpoints', []): if interfaces and endpoint['interface'] not in interfaces: continue if region_name and region_name != endpoint['region_name']: continue if endpoint_id and endpoint_id != endpoint['id']: continue if not endpoint['url']: continue matching_endpoints[service['type']].append( discover.EndpointData( catalog_url=endpoint['url'], service_type=service['type'], service_name=service['name'], service_id=service['id'], interface=endpoint['interface'], region_name=endpoint['region_name'], endpoint_id=endpoint['id'], raw_endpoint=endpoint['raw_endpoint'])) if not interfaces: return self._endpoints_by_type(service_type, matching_endpoints) ret = {} for matched_service_type, endpoints in matching_endpoints.items(): if not endpoints: ret[matched_service_type] = [] continue matches_by_interface = {} for endpoint in endpoints: matches_by_interface.setdefault(endpoint.interface, []) matches_by_interface[endpoint.interface].append(endpoint) best_interface = [i for i in interfaces if i in matches_by_interface.keys()][0] ret[matched_service_type] = matches_by_interface[best_interface] return self._endpoints_by_type(service_type, ret) def _endpoints_by_type(self, requested, endpoints): """Get the approrpriate endpoints from the list of given endpoints. Per the service type alias rules: If a user requests a service by its proper name and that matches, win. If a user requests a service by its proper name and only a single alias matches, win. If a user requests a service by its proper name and more than one alias matches, choose the first alias from the list given. Do the "first alias" match after the other filters, as they might limit the number of choices for us otherwise. :param str requested: The service_type as requested by the user. :param dict sc: A dictionary keyed by found service_type. Values are opaque to this method. :returns: Dict of service_type/endpoints filtered for the appropriate service_type based on alias matching rules. """ if not requested or not discover._SERVICE_TYPES.is_known(requested): # The user did not request a service we have any alias information # about, or did not request a service, which means that we cannot # further filter the list. return endpoints if len(endpoints) < 2: # There is at most one type found from the initial pass through # the catalog. Nothing further to do. return endpoints # At this point, the user has requested a type, we do know things # about aliases for that type, and we've found more than one match. # We must filter out additional types, otherwise clouds that register # the same endpoint twice as part of a migration will confuse users. # Only return the one the user requested if there's an exact match # and there is data for it. There might not be data for this match # if there are other filters that excluded it from consideration # after we accepted it as an alias. if endpoints.get(requested): return {requested: endpoints[requested]} # We've matched something that isn't exactly what the user requested. # Look at the possible types for this service in order or priority and # return the first match. for alias in discover._SERVICE_TYPES.get_all_types(requested): if endpoints.get(alias): # Return the first one found in the order listed. return {alias: endpoints[alias]} # We should never get here - it's a programming logic error on our # part if we do. Raise this so that we can panic in unit tests. raise ValueError("Programming error choosing an endpoint.") def get_endpoints(self, service_type=None, interface=None, region_name=None, service_name=None, service_id=None, endpoint_id=None): """Fetch and filter endpoint data for the specified service(s). Returns endpoints for the specified service (or all) containing the specified type (or all) and region (or all) and service name. If there is no name in the service catalog the service_name check will be skipped. This allows compatibility with services that existed before the name was available in the catalog. Returns a dict keyed by service_type with a list of endpoint dicts """ endpoints_data = self.get_endpoints_data( service_type=service_type, interface=interface, region_name=region_name, service_name=service_name, service_id=service_id, endpoint_id=endpoint_id) endpoints = {} for service_type, data in endpoints_data.items(): endpoints[service_type] = self._denormalize_endpoints(data) return endpoints def get_endpoint_data_list(self, service_type=None, interface='public', region_name=None, service_name=None, service_id=None, endpoint_id=None): """Fetch a flat list of matching EndpointData objects. Fetch the endpoints from the service catalog for a particular endpoint attribute. If no attribute is given, return the first endpoint of the specified type. Valid interface types: `public` or `publicURL`, `internal` or `internalURL`, `admin` or 'adminURL` :param string service_type: Service type of the endpoint. :param interface: Type of endpoint. Can be a single value or a list of values. If it's a list of values, they will be looked for in order of preference. :param string region_name: Region of the endpoint. :param string service_name: The assigned name of the service. :param string service_id: The identifier of a service. :param string endpoint_id: The identifier of an endpoint. :returns: a list of matching EndpointData objects :rtype: list(`keystoneauth1.discover.EndpointData`) """ endpoints = self.get_endpoints_data(service_type=service_type, interface=interface, region_name=region_name, service_name=service_name, service_id=service_id, endpoint_id=endpoint_id) return [endpoint for data in endpoints.values() for endpoint in data] def get_urls(self, service_type=None, interface='public', region_name=None, service_name=None, service_id=None, endpoint_id=None): """Fetch endpoint urls from the service catalog. Fetch the urls of endpoints from the service catalog for a particular endpoint attribute. If no attribute is given, return the url of the first endpoint of the specified type. Valid interface types: `public` or `publicURL`, `internal` or `internalURL`, `admin` or 'adminURL` :param string service_type: Service type of the endpoint. :param interface: Type of endpoint. Can be a single value or a list of values. If it's a list of values, they will be looked for in order of preference. :param string region_name: Region of the endpoint. :param string service_name: The assigned name of the service. :param string service_id: The identifier of a service. :param string endpoint_id: The identifier of an endpoint. :returns: tuple of urls """ endpoints = self.get_endpoint_data_list(service_type=service_type, interface=interface, region_name=region_name, service_name=service_name, service_id=service_id, endpoint_id=endpoint_id) return tuple([endpoint.url for endpoint in endpoints]) def url_for(self, service_type=None, interface='public', region_name=None, service_name=None, service_id=None, endpoint_id=None): """Fetch an endpoint from the service catalog. Fetch the specified endpoint from the service catalog for a particular endpoint attribute. If no attribute is given, return the first endpoint of the specified type. Valid interface types: `public` or `publicURL`, `internal` or `internalURL`, `admin` or 'adminURL` :param string service_type: Service type of the endpoint. :param interface: Type of endpoint. Can be a single value or a list of values. If it's a list of values, they will be looked for in order of preference. :param string region_name: Region of the endpoint. :param string service_name: The assigned name of the service. :param string service_id: The identifier of a service. :param string endpoint_id: The identifier of an endpoint. """ return self.endpoint_data_for(service_type=service_type, interface=interface, region_name=region_name, service_name=service_name, service_id=service_id, endpoint_id=endpoint_id).url def endpoint_data_for(self, service_type=None, interface='public', region_name=None, service_name=None, service_id=None, endpoint_id=None): """Fetch endpoint data from the service catalog. Fetch the specified endpoint data from the service catalog for a particular endpoint attribute. If no attribute is given, return the first endpoint of the specified type. Valid interface types: `public` or `publicURL`, `internal` or `internalURL`, `admin` or 'adminURL` :param string service_type: Service type of the endpoint. :param interface: Type of endpoint. Can be a single value or a list of values. If it's a list of values, they will be looked for in order of preference. :param string region_name: Region of the endpoint. :param string service_name: The assigned name of the service. :param string service_id: The identifier of a service. :param string endpoint_id: The identifier of an endpoint. """ if not self._catalog: raise exceptions.EmptyCatalog('The service catalog is empty.') endpoint_data_list = self.get_endpoint_data_list( service_type=service_type, interface=interface, region_name=region_name, service_name=service_name, service_id=service_id, endpoint_id=endpoint_id) if endpoint_data_list: return endpoint_data_list[0] if service_name and region_name: msg = ('%(interface)s endpoint for %(service_type)s service ' 'named %(service_name)s in %(region_name)s region not ' 'found' % {'interface': interface, 'service_type': service_type, 'service_name': service_name, 'region_name': region_name}) elif service_name: msg = ('%(interface)s endpoint for %(service_type)s service ' 'named %(service_name)s not found' % {'interface': interface, 'service_type': service_type, 'service_name': service_name}) elif region_name: msg = ('%(interface)s endpoint for %(service_type)s service ' 'in %(region_name)s region not found' % {'interface': interface, 'service_type': service_type, 'region_name': region_name}) else: msg = ('%(interface)s endpoint for %(service_type)s service ' 'not found' % {'interface': interface, 'service_type': service_type}) raise exceptions.EndpointNotFound(msg) class ServiceCatalogV2(ServiceCatalog): """An object for encapsulating the v2 service catalog. The object is created using raw v2 auth token from Keystone. """ @classmethod def from_token(cls, token): if 'access' not in token: raise ValueError('Invalid token format for fetching catalog') return cls(token['access'].get('serviceCatalog', {})) @staticmethod def normalize_interface(interface): if interface and 'URL' not in interface: interface += 'URL' return interface def is_interface_match(self, endpoint, interface): return interface in endpoint def _normalize_endpoints(self, endpoints): """Translate endpoint description dicts into v3 form. Takes a raw endpoint description from the catalog and changes it to be in v3 format. It also saves a copy of the data in raw_endpoint so that it can be returned by methods that expect the actual original data. :param list endpoints: List of endpoint description dicts :returns: List of endpoint description dicts in v3 format """ new_endpoints = [] for endpoint in endpoints: raw_endpoint = endpoint.copy() interface_urls = {} interface_keys = [key for key in endpoint.keys() if key.endswith('URL')] for key in interface_keys: interface = self.normalize_interface(key) interface_urls[interface] = endpoint.pop(key) for interface, url in interface_urls.items(): new_endpoint = endpoint.copy() new_endpoint['interface'] = interface new_endpoint['url'] = url # Save the actual endpoint for ease of later reconstruction new_endpoint['raw_endpoint'] = raw_endpoint new_endpoints.append(new_endpoint) return new_endpoints def _denormalize_endpoints(self, endpoints): """Return original endpoint description dicts. Takes a list of EndpointData objects and returns the original dict that was returned from the catalog. :param list endpoints: List of `keystoneauth1.discover.EndpointData` :returns: List of endpoint description dicts in original catalog format """ raw_endpoints = super(ServiceCatalogV2, self)._denormalize_endpoints( endpoints) # The same raw endpoint content will be in the list once for each # v2 endpoint_type entry. We only need one of them in the resulting # list. So keep a list of the string versions. seen = {} endpoints = [] for endpoint in raw_endpoints: if str(endpoint) in seen: continue seen[str(endpoint)] = True endpoints.append(endpoint) return endpoints class ServiceCatalogV3(ServiceCatalog): """An object for encapsulating the v3 service catalog. The object is created using raw v3 auth token from Keystone. """ @classmethod def from_token(cls, token): if 'token' not in token: raise ValueError('Invalid token format for fetching catalog') return cls(token['token'].get('catalog', {})) @staticmethod def normalize_interface(interface): if interface: interface = interface.rstrip('URL') return interface def is_interface_match(self, endpoint, interface): try: return interface == endpoint['interface'] except KeyError: return False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/access/service_providers.py0000664000175000017500000000306600000000000024527 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from keystoneauth1 import exceptions class ServiceProviders(object): """Helper methods for dealing with Service Providers.""" @classmethod def from_token(cls, token): if 'token' not in token: raise ValueError('Token format does not support service' 'providers.') return cls(token['token'].get('service_providers', [])) def __init__(self, service_providers): def normalize(service_providers_list): return dict((sp['id'], sp) for sp in service_providers_list if 'id' in sp) self._service_providers = normalize(service_providers) def _get_service_provider(self, sp_id): try: return self._service_providers[sp_id] except KeyError: raise exceptions.ServiceProviderNotFound(sp_id) def get_sp_url(self, sp_id): return self._get_service_provider(sp_id).get('sp_url') def get_auth_url(self, sp_id): return self._get_service_provider(sp_id).get('auth_url') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/adapter.py0000664000175000017500000005751000000000000021154 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import warnings import requests from keystoneauth1 import _fair_semaphore from keystoneauth1 import session class Adapter(object): """An instance of a session with local variables. A session is a global object that is shared around amongst many clients. It therefore contains state that is relevant to everyone. There is a lot of state such as the service type and region_name that are only relevant to a particular client that is using the session. An adapter provides a wrapper of client local data around the global session object. version, min_version, max_version and default_microversion can all be given either as a string or a tuple. :param session: The session object to wrap. :type session: keystoneauth1.session.Session :param str service_type: The default service_type for URL discovery. :param str service_name: The default service_name for URL discovery. :param str interface: The default interface for URL discovery. :param str region_name: The default region_name for URL discovery. :param str endpoint_override: Always use this endpoint URL for requests for this client. :param version: The minimum version restricted to a given Major API. Mutually exclusive with min_version and max_version. (optional) :param auth: An auth plugin to use instead of the session one. :type auth: keystoneauth1.plugin.BaseAuthPlugin :param str user_agent: The User-Agent string to set. :param int connect_retries: The maximum number of retries that should be attempted for connection errors. Default None - use session default which is don't retry. :param logger: A logging object to use for requests that pass through this adapter. :type logger: logging.Logger :param dict allow: Extra filters to pass when discovering API versions. (optional) :param dict additional_headers: Additional headers that should be attached to every request passing through the adapter. Headers of the same name specified per request will take priority. :param str client_name: The name of the client that created the adapter. This will be used to create the user_agent. :param str client_version: The version of the client that created the adapter. This will be used to create the user_agent. :param bool allow_version_hack: Allow keystoneauth to hack up catalog URLS to support older schemes. (optional, default True) :param str global_request_id: A global_request_id (in the form of ``req-$uuid``) that will be passed on all requests. Enables cross project request id tracking. :param min_version: The minimum major version of a given API, intended to be used as the lower bound of a range with max_version. Mutually exclusive with version. If min_version is given with no max_version it is as if max version is 'latest'. (optional) :param max_version: The maximum major version of a given API, intended to be used as the upper bound of a range with min_version. Mutually exclusive with version. (optional) :param default_microversion: The default microversion value to send with API requests. While microversions are a per-request feature, a user may know they want to default to sending a specific value. (optional) :param int status_code_retries: The maximum number of retries that should be attempted for retriable HTTP status codes (optional, defaults to 0 - never retry). :param list retriable_status_codes: List of HTTP status codes that should be retried (optional, defaults to HTTP 503, has no effect when status_code_retries is 0). :param bool raise_exc: If True, requests returning failing HTTP responses will raise an exception; if False, the response is returned. This can be overridden on a per-request basis via the kwarg of the same name. :param float rate_limit: A client-side rate limit to impose on requests made through this adapter in requests per second. For instance, a rate_limit of 2 means to allow no more than 2 requests per second, and a rate_limit of 0.5 means to allow no more than 1 request every two seconds. (optional, defaults to None, which means no rate limiting will be applied). :param int concurrency: How many simultaneous http requests this Adapter can be used for. (optional, defaults to None, which means no limit). :param float connect_retry_delay: Delay (in seconds) between two connect retries (if enabled). By default exponential retry starting with 0.5 seconds up to a maximum of 60 seconds is used. :param float status_code_retry_delay: Delay (in seconds) between two status code retries (if enabled). By default exponential retry starting with 0.5 seconds up to a maximum of 60 seconds is used. """ client_name = None client_version = None def __init__(self, session, service_type=None, service_name=None, interface=None, region_name=None, endpoint_override=None, version=None, auth=None, user_agent=None, connect_retries=None, logger=None, allow=None, additional_headers=None, client_name=None, client_version=None, allow_version_hack=None, global_request_id=None, min_version=None, max_version=None, default_microversion=None, status_code_retries=None, retriable_status_codes=None, raise_exc=None, rate_limit=None, concurrency=None, connect_retry_delay=None, status_code_retry_delay=None, ): if version and (min_version or max_version): raise TypeError( "version is mutually exclusive with min_version and" " max_version") # NOTE(jamielennox): when adding new parameters to adapter please also # add them to the adapter call in httpclient.HTTPClient.__init__ as # well as to load_adapter_from_argparse below if the argument is # intended to be something a user would reasonably expect to set on # a command line self.session = session self.service_type = service_type self.service_name = service_name self.interface = interface self.region_name = region_name self.endpoint_override = endpoint_override self.version = version self.user_agent = user_agent self.auth = auth self.connect_retries = connect_retries self.logger = logger self.allow = allow or {} self.additional_headers = additional_headers or {} self.allow_version_hack = allow_version_hack self.min_version = min_version self.max_version = max_version self.default_microversion = default_microversion self.status_code_retries = status_code_retries self.retriable_status_codes = retriable_status_codes self.connect_retry_delay = connect_retry_delay self.status_code_retry_delay = status_code_retry_delay self.raise_exc = raise_exc self.global_request_id = global_request_id if client_name: self.client_name = client_name if client_version: self.client_version = client_version rate_delay = 0.0 if rate_limit: # 1 / rate converts from requests per second to delay # between requests needed to achieve that rate. rate_delay = 1.0 / rate_limit self._rate_semaphore = _fair_semaphore.FairSemaphore( concurrency, rate_delay) def _set_endpoint_filter_kwargs(self, kwargs): if self.service_type: kwargs.setdefault('service_type', self.service_type) if self.service_name: kwargs.setdefault('service_name', self.service_name) if self.interface: kwargs.setdefault('interface', self.interface) if self.region_name: kwargs.setdefault('region_name', self.region_name) if self.version: kwargs.setdefault('version', self.version) if self.min_version: kwargs.setdefault('min_version', self.min_version) if self.max_version: kwargs.setdefault('max_version', self.max_version) if self.allow_version_hack is not None: kwargs.setdefault('allow_version_hack', self.allow_version_hack) def request(self, url, method, **kwargs): endpoint_filter = kwargs.setdefault('endpoint_filter', {}) self._set_endpoint_filter_kwargs(endpoint_filter) # NOTE(gmann): Convert r initlize the headers to # CaseInsensitiveDict to make sure headers are # case insensitive. if kwargs.get('headers'): kwargs['headers'] = requests.structures.CaseInsensitiveDict( kwargs['headers']) else: kwargs['headers'] = requests.structures.CaseInsensitiveDict() if self.endpoint_override: kwargs.setdefault('endpoint_override', self.endpoint_override) if self.auth: kwargs.setdefault('auth', self.auth) if self.user_agent: kwargs.setdefault('user_agent', self.user_agent) for arg in ('connect_retries', 'status_code_retries', 'connect_retry_delay', 'status_code_retry_delay'): if getattr(self, arg) is not None: kwargs.setdefault(arg, getattr(self, arg)) if self.retriable_status_codes: kwargs.setdefault('retriable_status_codes', self.retriable_status_codes) if self.logger: kwargs.setdefault('logger', self.logger) if self.allow: kwargs.setdefault('allow', self.allow) if self.default_microversion is not None: kwargs.setdefault('microversion', self.default_microversion) if isinstance(self.session, (session.Session, Adapter)): # these things are unsupported by keystoneclient's session so be # careful with them until everyone has transitioned to ksa. # Allowing adapter allows adapter nesting that auth_token does. if self.client_name: kwargs.setdefault('client_name', self.client_name) if self.client_version: kwargs.setdefault('client_version', self.client_version) if self._rate_semaphore: kwargs.setdefault('rate_semaphore', self._rate_semaphore) else: warnings.warn('Using keystoneclient sessions has been deprecated. ' 'Please update your software to use keystoneauth1.') for k, v in self.additional_headers.items(): kwargs.setdefault('headers', {}).setdefault(k, v) if self.global_request_id is not None: kwargs.setdefault('headers', {}).setdefault( "X-OpenStack-Request-ID", self.global_request_id) if self.raise_exc is not None: kwargs.setdefault('raise_exc', self.raise_exc) return self.session.request(url, method, **kwargs) def get_token(self, auth=None): """Return a token as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth.AuthorizationFailure: if a new token fetch fails. :returns: A valid token. :rtype: :class:`str` """ return self.session.get_token(auth or self.auth) def get_endpoint(self, auth=None, **kwargs): """Get an endpoint as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :returns: An endpoint if available or None. :rtype: :class:`str` """ if self.endpoint_override: return self.endpoint_override self._set_endpoint_filter_kwargs(kwargs) return self.session.get_endpoint(auth or self.auth, **kwargs) def get_endpoint_data(self, auth=None): """Get the endpoint data for this Adapter's endpoint. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :raises TypeError: If arguments are invalid :returns: Endpoint data if available or None. :rtype: keystoneauth1.discover.EndpointData """ kwargs = {} self._set_endpoint_filter_kwargs(kwargs) if self.endpoint_override: kwargs['endpoint_override'] = self.endpoint_override return self.session.get_endpoint_data(auth or self.auth, **kwargs) def get_all_version_data( self, interface='public', region_name=None): """Get data about all versions of a service. :param interface: Type of endpoint to get version data for. Can be a single value or a list of values. A value of None indicates that all interfaces should be queried. (optional, defaults to public) :param string region_name: Region of endpoints to get version data for. A valueof None indicates that all regions should be queried. (optional, defaults to None) :returns: A dictionary keyed by region_name with values containing dictionaries keyed by interface with values being a list of :class:`~keystoneauth1.discover.VersionData`. """ return self.session.get_all_version_data( interface=interface, region_name=region_name, service_type=self.service_type) def get_api_major_version(self, auth=None, **kwargs): """Get the major API version as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :return: The major version of the API of the service discovered. :rtype: tuple or None """ self._set_endpoint_filter_kwargs(kwargs) if self.endpoint_override: kwargs['endpoint_override'] = self.endpoint_override return self.session.get_api_major_version(auth or self.auth, **kwargs) def invalidate(self, auth=None): """Invalidate an authentication plugin.""" return self.session.invalidate(auth or self.auth) def get_user_id(self, auth=None): """Return the authenticated user_id as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth.AuthorizationFailure: if a new token fetch fails. :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :returns: Current `user_id` or None if not supported by plugin. :rtype: :class:`str` """ return self.session.get_user_id(auth or self.auth) def get_project_id(self, auth=None): """Return the authenticated project_id as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth.AuthorizationFailure: if a new token fetch fails. :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :returns: Current `project_id` or None if not supported by plugin. :rtype: :class:`str` """ return self.session.get_project_id(auth or self.auth) def get(self, url, **kwargs): return self.request(url, 'GET', **kwargs) def head(self, url, **kwargs): return self.request(url, 'HEAD', **kwargs) def post(self, url, **kwargs): return self.request(url, 'POST', **kwargs) def put(self, url, **kwargs): return self.request(url, 'PUT', **kwargs) def patch(self, url, **kwargs): return self.request(url, 'PATCH', **kwargs) def delete(self, url, **kwargs): return self.request(url, 'DELETE', **kwargs) # TODO(efried): Move this to loading.adapter.Adapter @classmethod def register_argparse_arguments(cls, parser, service_type=None): """Attach arguments to a given argparse Parser for Adapters. :param parser: The argparse parser to attach options to. :type parser: argparse.ArgumentParser :param str service_type: Default service_type value. (optional) """ adapter_group = parser.add_argument_group( 'Service Options', 'Options controlling the specialization of the API' ' Connection from information found in the catalog') adapter_group.add_argument( '--os-service-type', metavar='', default=os.environ.get('OS_SERVICE_TYPE', service_type), help='Service type to request from the catalog') adapter_group.add_argument( '--os-service-name', metavar='', default=os.environ.get('OS_SERVICE_NAME', None), help='Service name to request from the catalog') adapter_group.add_argument( '--os-interface', metavar='', default=os.environ.get('OS_INTERFACE', 'public'), help='API Interface to use [public, internal, admin]') adapter_group.add_argument( '--os-region-name', metavar='', default=os.environ.get('OS_REGION_NAME', None), help='Region of the cloud to use') adapter_group.add_argument( '--os-endpoint-override', metavar='', default=os.environ.get('OS_ENDPOINT_OVERRIDE', None), help='Endpoint to use instead of the endpoint in the catalog') adapter_group.add_argument( '--os-api-version', metavar='', default=os.environ.get('OS_API_VERSION', None), help='Which version of the service API to use') # TODO(efried): Move this to loading.adapter.Adapter @classmethod def register_service_argparse_arguments(cls, parser, service_type): """Attach arguments to a given argparse Parser for Adapters. :param parser: The argparse parser to attach options to. :type parser: argparse.ArgumentParser :param str service_type: Name of a service to generate additional arguments for. """ service_env = service_type.upper().replace('-', '_') adapter_group = parser.add_argument_group( '{service_type} Service Options'.format( service_type=service_type.title()), 'Options controlling the specialization of the {service_type}' ' API Connection from information found in the catalog'.format( service_type=service_type.title())) adapter_group.add_argument( '--os-{service_type}-service-type'.format( service_type=service_type), metavar='', default=os.environ.get( 'OS_{service_type}_SERVICE_TYPE'.format( service_type=service_env), None), help=('Service type to request from the catalog for the' ' {service_type} service'.format( service_type=service_type))) adapter_group.add_argument( '--os-{service_type}-service-name'.format( service_type=service_type), metavar='', default=os.environ.get( 'OS_{service_type}_SERVICE_NAME'.format( service_type=service_env), None), help=('Service name to request from the catalog for the' ' {service_type} service'.format( service_type=service_type))) adapter_group.add_argument( '--os-{service_type}-interface'.format( service_type=service_type), metavar='', default=os.environ.get( 'OS_{service_type}_INTERFACE'.format( service_type=service_env), None), help=('API Interface to use for the {service_type} service' ' [public, internal, admin]'.format( service_type=service_type))) adapter_group.add_argument( '--os-{service_type}-api-version'.format( service_type=service_type), metavar='', default=os.environ.get( 'OS_{service_type}_API_VERSION'.format( service_type=service_env), None), help=('Which version of the service API to use for' ' the {service_type} service'.format( service_type=service_type))) adapter_group.add_argument( '--os-{service_type}-endpoint-override'.format( service_type=service_type), metavar='', default=os.environ.get( 'OS_{service_type}_ENDPOINT_OVERRIDE'.format( service_type=service_env), None), help=('Endpoint to use for the {service_type} service' ' instead of the endpoint in the catalog'.format( service_type=service_type))) class LegacyJsonAdapter(Adapter): """Make something that looks like an old HTTPClient. A common case when using an adapter is that we want an interface similar to the HTTPClients of old which returned the body as JSON as well. You probably don't want this if you are starting from scratch. """ def request(self, *args, **kwargs): headers = kwargs.setdefault('headers', {}) headers.setdefault('Accept', 'application/json') try: kwargs['json'] = kwargs.pop('body') except KeyError: pass resp = super(LegacyJsonAdapter, self).request(*args, **kwargs) try: body = resp.json() except ValueError: body = None return resp, body # TODO(efried): Deprecate this in favor of # loading.adapter.register_argparse_arguments def register_adapter_argparse_arguments(*args, **kwargs): return Adapter.register_argparse_arguments(*args, **kwargs) # TODO(efried): Deprecate this in favor of # loading.adapter.register_service_argparse_arguments def register_service_adapter_argparse_arguments(*args, **kwargs): return Adapter.register_service_argparse_arguments(*args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/discover.py0000664000175000017500000017242200000000000021352 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """The passive components to version discovery. The Discover object in discover.py contains functions that can create objects on your behalf. These functions are not usable from within the keystoneauth1 library because you will get dependency resolution issues. The Discover object in this file provides the querying components of Discovery. This includes functions like url_for which allow you to retrieve URLs and the raw data specified in version discovery responses. """ import copy import re import os_service_types import six from six.moves import urllib from keystoneauth1 import _utils as utils from keystoneauth1 import exceptions _LOGGER = utils.get_logger(__name__) LATEST = float('inf') _SERVICE_TYPES = os_service_types.ServiceTypes() def _str_or_latest(val): """Convert val to a string, handling LATEST => 'latest'. :param val: An int or the special value LATEST. :return: A string representation of val. If val was LATEST, the return is 'latest'. """ return 'latest' if val == LATEST else str(val) def _int_or_latest(val): """Convert val to an int or the special value LATEST. :param val: An int()-able, or the string 'latest', or the special value LATEST. :return: An int, or the special value LATEST """ return LATEST if val == 'latest' or val == LATEST else int(val) def get_version_data(session, url, authenticated=None): """Retrieve raw version data from a url. The return is a list of dicts of the form:: [{ 'status': 'STABLE', 'id': 'v2.3', 'links': [ { 'href': 'http://network.example.com/v2.3', 'rel': 'self', }, { 'href': 'http://network.example.com/', 'rel': 'collection', }, ], 'min_version': '2.0', 'max_version': '2.7', }, ..., ] Note: The maximum microversion may be specified by `max_version` or `version`, the former superseding the latter. All `*version` keys are optional. Other keys and 'links' entries are permitted, but ignored. :param session: A Session object that can be used for communication. :type session: keystoneauth1.session.Session :param string url: Endpoint or discovery URL from which to retrieve data. :param bool authenticated: Include a token in the discovery call. (optional) Defaults to None. :return: A list of dicts containing version information. :rtype: list(dict) """ headers = {'Accept': 'application/json'} try: resp = session.get(url, headers=headers, authenticated=authenticated) except exceptions.Unauthorized: resp = session.get(url, headers=headers, authenticated=True) try: body_resp = resp.json() except ValueError: pass else: # Swift returns the list of containers for an account on an # authenticated GET from /, not a version document. To our knowledge # it's the only thing returning a [] here - and that's ok. if isinstance(body_resp, list): raise exceptions.DiscoveryFailure( 'Invalid Response - List returned instead of dict') # In the event of querying a root URL we will get back a list of # available versions. try: return body_resp['versions']['values'] except (KeyError, TypeError): pass # Most servers don't have a 'values' element so accept a simple # versions dict if available. try: return body_resp['versions'] except KeyError: pass # Otherwise if we query an endpoint like /v2.0 then we will get back # just the one available version. try: return [body_resp['version']] except KeyError: pass # Older Ironic does not actually return a discovery document for the # single version discovery endpoint, which confuses the single-version # fallback logic. While there are no known other services returning # min/max ranges using headers instead of body, this is done in a # non-Ironic specific manner just in case. # The existence of this support should not be an indication to any # OpenStack services that they should ADD this. if 'id' in body_resp: body_resp['status'] = Status.CURRENT for header in resp.headers: # We lose the case-insensitive quality here header = header.lower() if not header.startswith('x-openstack'): continue # Once the body starts having these values, stop overriding # with the header values if header.endswith('api-minimum-version'): body_resp.setdefault('min_version', resp.headers[header]) if header.endswith('api-maximum-version'): body_resp.setdefault('version', resp.headers[header]) return [body_resp] err_text = resp.text[:50] + '...' if len(resp.text) > 50 else resp.text raise exceptions.DiscoveryFailure('Invalid Response - Bad version data ' 'returned: %s' % err_text) def normalize_version_number(version): """Turn a version representation into a tuple. Examples: The following all produce a return value of (1, 0):: 1, '1', 'v1', [1], (1,), ['1'], 1.0, '1.0', 'v1.0', (1, 0) The following all produce a return value of (1, 20, 3):: 'v1.20.3', '1.20.3', (1, 20, 3), ['1', '20', '3'] The following all produce a return value of (LATEST, LATEST):: 'latest', 'vlatest', ('latest', 'latest'), (LATEST, LATEST) The following all produce a return value of (2, LATEST):: '2.latest', 'v2.latest', (2, LATEST), ('2', 'latest') :param version: A version specifier in any of the following forms: String, possibly prefixed with 'v', containing one or more numbers *or* the string 'latest', separated by periods. Examples: 'v1', 'v1.2', '1.2.3', '123', 'latest', '1.latest', 'v1.latest'. Integer. This will be assumed to be the major version, with a minor version of 0. Float. The integer part is assumed to be the major version; the decimal part the minor version. Non-string iterable comprising integers, integer strings, the string 'latest', or the special value LATEST. Examples: (1,), [1, 2], ('12', '34', '56'), (LATEST,), (2, 'latest') :return: A tuple of len >= 2 comprising integers and/or LATEST. :raises TypeError: If the input version cannot be interpreted. """ # Copy the input var so the error presents the original value ver = version # If it's a non-string iterable, turn it into a string for subsequent # processing. This ensures at least 1 decimal point if e.g. [1] is given. if not isinstance(ver, six.string_types): try: ver = '.'.join(map(_str_or_latest, ver)) except TypeError: # Not an iterable pass # If it's a numeric or an integer as a string then normalize it to a # float string. This ensures 1 decimal point. # If it's a float as a string, don't do that, the split/map below will do # what we want. (Otherwise, we wind up with 3.20 -> (3, 2)) if isinstance(ver, six.string_types): # trim the v from a 'v2.0' or similar ver = ver.lstrip('v') try: # If version is a pure int, like '1' or '200' this will produce # a stringified version with a .0 added. If it's any other number, # such as '1.1' - int(version) raises an Exception ver = str(float(int(ver))) except ValueError: pass # If it's an int or float, turn it into a float string elif isinstance(ver, (int, float)): ver = _str_or_latest(float(ver)) # At this point, we should either have a string that contains numbers with # at least one decimal point, or something decidedly else. # if it's a string from above break it on . try: ver = ver.split('.') except AttributeError: # Not a string pass # Handle special case variants of just 'latest' if ver == 'latest' or tuple(ver) == ('latest',): return LATEST, LATEST # It's either an interable, or something else that makes us sad. try: return tuple(map(_int_or_latest, ver)) except (TypeError, ValueError): pass raise TypeError('Invalid version specified: %s' % version) def _normalize_version_args( version, min_version, max_version, service_type=None): # The sins of our fathers become the blood on our hands. # If a user requests an old-style service type such as volumev2, then they # are inherently requesting the major API version 2. It's not a good # interface, but it's the one that was imposed on the world years ago # because the client libraries all hid the version discovery document. # In order to be able to ensure that a user who requests volumev2 does not # get a block-storage endpoint that only provides v3 of the block-storage # service, we need to pull the version out of the service_type. The # service-types-authority will prevent the growth of new monstrosities such # as this, but in order to move forward without breaking people, we have # to just cry in the corner while striking ourselves with thorned branches. # That said, for sure only do this hack for officially known service_types. if (service_type and _SERVICE_TYPES.is_known(service_type) and service_type[-1].isdigit() and service_type[-2] == 'v'): implied_version = normalize_version_number(service_type[-1]) else: implied_version = None if version and (min_version or max_version): raise ValueError( "version is mutually exclusive with min_version and max_version") if version: # Explode this into min_version and max_version min_version = normalize_version_number(version) max_version = (min_version[0], LATEST) if implied_version: if min_version[0] != implied_version[0]: raise exceptions.ImpliedVersionMismatch( service_type=service_type, implied=implied_version, given=version_to_string(version)) return min_version, max_version if min_version == 'latest': if max_version not in (None, 'latest'): raise ValueError( "min_version is 'latest' and max_version is {max_version}" " but is only allowed to be 'latest' or None".format( max_version=max_version)) max_version = 'latest' # Normalize e.g. empty string to None min_version = min_version or None max_version = max_version or None if min_version: min_version = normalize_version_number(min_version) # If min_version was specified but max_version was not, max is latest. max_version = normalize_version_number(max_version or 'latest') # NOTE(efried): We should be doing this instead: # max_version = normalize_version_number(max_version or 'latest') # However, see first NOTE(jamielennox) in EndpointData._set_version_info. if max_version: max_version = normalize_version_number(max_version) if None not in (min_version, max_version) and max_version < min_version: raise ValueError("min_version cannot be greater than max_version") if implied_version: if min_version: if min_version[0] != implied_version[0]: raise exceptions.ImpliedMinVersionMismatch( service_type=service_type, implied=implied_version, given=version_to_string(min_version)) else: min_version = implied_version # If 'latest' is provided with a versioned service-type like # volumev2 - the user wants the latest of volumev2, not the latest # of block-storage. if max_version and max_version[0] != LATEST: if max_version[0] != implied_version[0]: raise exceptions.ImpliedMaxVersionMismatch( service_type=service_type, implied=implied_version, given=version_to_string(max_version)) else: max_version = (implied_version[0], LATEST) return min_version, max_version def version_to_string(version): """Turn a version tuple into a string. :param tuple version: A version represented as a tuple of ints. As a special case, a tuple member may be LATEST, which translates to 'latest'. :return: A version represented as a period-delimited string. """ # Special case if all(ver == LATEST for ver in version): return 'latest' return ".".join(map(_str_or_latest, version)) def version_between(min_version, max_version, candidate): """Determine whether a candidate version is within a specified range. :param min_version: The minimum version that is acceptable. None/empty indicates no lower bound. :param max_version: The maximum version that is acceptable. None/empty indicates no upper bound. :param candidate: Candidate version to test. May not be None/empty. :return: True if candidate is between min_version and max_version; False otherwise. :raises ValueError: If candidate is None. :raises TypeError: If any input cannot be normalized. """ if not candidate: raise ValueError("candidate is required.") candidate = normalize_version_number(candidate) # Normalize up front to validate any malformed inputs if min_version: min_version = normalize_version_number(min_version) if max_version: max_version = normalize_version_number(max_version) # If the candidate is less than the min_version, it's not a match. # No min_version means no lower bound. if min_version and candidate < min_version: return False # If the candidate is higher than the max_version, it's not a match. # No max_version means no upper bound. if max_version and candidate > max_version: return False return True def version_match(required, candidate): """Test that an available version satisfies the required version. To be suitable a version must be of the same major version as required and be at least a match in minor/patch level. eg. 3.3 is a match for a required 3.1 but 4.1 is not. :param tuple required: the version that must be met. :param tuple candidate: the version to test against required. :returns: True if candidate is suitable False otherwise. :rtype: bool """ # major versions must be the same (e.g. even though v2 is a lower # version than v3 we can't use it if v2 was requested) if candidate[0] != required[0]: return False # prevent selecting a minor version less than what is required if candidate < required: return False return True def _latest_soft_match(required, candidate): if not required: return False if LATEST not in required: return False if all(part == LATEST for part in required): return True if required[0] == candidate[0] and required[1] == LATEST: return True # TODO(efried): Do we need to handle >2-part version numbers here? return False def _combine_relative_url(discovery_url, version_url): # NOTE(jamielennox): urllib.parse.urljoin allows the url to be relative # or even protocol-less. The additional trailing '/' makes urljoin respect # the current path as canonical even if the url doesn't include it. for # example a "v2" path from http://host/admin should resolve as # http://host/admin/v2 where it would otherwise be host/v2. This has no # effect on absolute urls. url = urllib.parse.urljoin(discovery_url.rstrip('/') + '/', version_url) # Sadly version discovery documents are common with the scheme # and netloc broken. parsed_version_url = urllib.parse.urlparse(url) parsed_discovery_url = urllib.parse.urlparse(discovery_url) # The services can override the version_url with some config options.(for # example, In Keystone, Cinder and Glance, the option is "public_endpoint", # and "compute_link_prefix", "network_link_prefix" in Nova and Neutron. # In this case, it's hard to distinguish which part in version_url is # useful for discovery_url , so here we just get the version from # version_url and then add it into the discovery_url if needed. path = parsed_version_url.path if parsed_discovery_url.netloc != parsed_version_url.netloc: version = version_url.rstrip('/').split('/')[-1] url_path = parsed_discovery_url.path.rstrip('/') if not url_path.endswith(version): path = url_path + '/' + version if version_url.endswith('/'): # add '/' back to keep backward compatibility. path = path + '/' else: path = parsed_discovery_url.path return urllib.parse.ParseResult( parsed_discovery_url.scheme, parsed_discovery_url.netloc, path, parsed_version_url.params, parsed_version_url.query, parsed_version_url.fragment).geturl() def _version_from_url(url): if not url: return url url = urllib.parse.urlparse(url) for part in reversed(url.path.split('/')): try: # All integer project ids can parse as valid versions. In URLs # all known instances of versions start with a v. So check to make # sure the url part starts with 'v', then check that it parses # as a valid version. if part[0] != 'v': continue return normalize_version_number(part) except Exception: pass return None class Status(object): CURRENT = 'CURRENT' SUPPORTED = 'SUPPORTED' DEPRECATED = 'DEPRECATED' EXPERIMENTAL = 'EXPERIMENTAL' UNKNOWN = 'UNKNOWN' KNOWN = (CURRENT, SUPPORTED, DEPRECATED, EXPERIMENTAL) @classmethod def normalize(cls, raw_status): """Turn a status into a canonical status value. If the status from the version discovery document does not match one of the known values, it will be set to 'UNKNOWN'. :param str raw_status: Status value from a discovery document. :returns: A canonicalized version of the status. Valid values are CURRENT, SUPPORTED, DEPRECATED, EXPERIMENTAL and UNKNOWN :rtype: str """ status = raw_status.upper() if status == 'STABLE': status = cls.CURRENT if status not in cls.KNOWN: status = cls.UNKNOWN return status class Discover(object): CURRENT_STATUSES = ('stable', 'current', 'supported') DEPRECATED_STATUSES = ('deprecated',) EXPERIMENTAL_STATUSES = ('experimental',) def __init__(self, session, url, authenticated=None): self._url = url self._data = get_version_data(session, url, authenticated=authenticated) def raw_version_data(self, allow_experimental=False, allow_deprecated=True, allow_unknown=False): """Get raw version information from URL. Raw data indicates that only minimal validation processing is performed on the data, so what is returned here will be the data in the same format it was received from the endpoint. :param bool allow_experimental: Allow experimental version endpoints. :param bool allow_deprecated: Allow deprecated version endpoints. :param bool allow_unknown: Allow endpoints with an unrecognised status. :returns: The endpoints returned from the server that match the criteria. :rtype: list """ versions = [] for v in self._data: try: status = v['status'] except KeyError: _LOGGER.warning('Skipping over invalid version data. ' 'No stability status in version.') continue status = status.lower() if status in self.CURRENT_STATUSES: versions.append(v) elif status in self.DEPRECATED_STATUSES: if allow_deprecated: versions.append(v) elif status in self.EXPERIMENTAL_STATUSES: if allow_experimental: versions.append(v) elif allow_unknown: versions.append(v) return versions def version_data(self, reverse=False, **kwargs): """Get normalized version data. Return version data in a structured way. :param bool reverse: Reverse the list. reverse=true will mean the returned list is sorted from newest to oldest version. :returns: A list of :class:`VersionData` sorted by version number. :rtype: list(VersionData) """ data = self.raw_version_data(**kwargs) versions = [] for v in data: try: version_str = v['id'] except KeyError: _LOGGER.info('Skipping invalid version data. Missing ID.') continue try: links = v['links'] except KeyError: _LOGGER.info('Skipping invalid version data. Missing links') continue version_number = normalize_version_number(version_str) # collect microversion information # NOTE(efried): Some existing discovery documents (e.g. from nova # v2.0 in the pike release) include *version keys with "" (empty # string) values, expecting them to be treated the same as if the # keys were absent. min_microversion = v.get('min_version') or None if min_microversion: min_microversion = normalize_version_number(min_microversion) max_microversion = v.get('max_version') if not max_microversion: max_microversion = v.get('version') or None if max_microversion: max_microversion = normalize_version_number(max_microversion) next_min_version = v.get('next_min_version') or None if next_min_version: next_min_version = normalize_version_number(next_min_version) not_before = v.get('not_before') or None self_url = None collection_url = None for link in links: try: rel = link['rel'] url = _combine_relative_url(self._url, link['href']) except (KeyError, TypeError): _LOGGER.info('Skipping invalid version link. ' 'Missing link URL or relationship.') continue if rel.lower() == 'self': self_url = url elif rel.lower() == 'collection': collection_url = url if not self_url: _LOGGER.info('Skipping invalid version data. ' 'Missing link to endpoint.') continue versions.append( VersionData(version=version_number, url=self_url, collection=collection_url, min_microversion=min_microversion, max_microversion=max_microversion, next_min_version=next_min_version, not_before=not_before, status=Status.normalize(v['status']), raw_status=v['status'])) versions.sort(key=lambda v: v['version'], reverse=reverse) return versions def version_string_data(self, reverse=False, **kwargs): """Get normalized version data with versions as strings. Return version data in a structured way. :param bool reverse: Reverse the list. reverse=true will mean the returned list is sorted from newest to oldest version. :returns: A list of :class:`VersionData` sorted by version number. :rtype: list(VersionData) """ version_data = self.version_data(reverse=reverse, **kwargs) for version in version_data: for key in ('version', 'min_microversion', 'max_microversion'): if version[key]: version[key] = version_to_string(version[key]) return version_data def data_for(self, version, **kwargs): """Return endpoint data for a version. NOTE: This method raises a TypeError if version is None. It is kept for backwards compatability. New code should use versioned_data_for instead. :param tuple version: The version is always a minimum version in the same major release as there should be no compatibility issues with using a version newer than the one asked for. :returns: the endpoint data for a URL that matches the required version (the format is described in version_data) or None if no match. :rtype: dict """ version = normalize_version_number(version) for data in self.version_data(reverse=True, **kwargs): # Since the data is reversed, the latest version is first. If # latest was requested, return it. if _latest_soft_match(version, data['version']): return data if version_match(version, data['version']): return data return None def url_for(self, version, **kwargs): """Get the endpoint url for a version. NOTE: This method raises a TypeError if version is None. It is kept for backwards compatability. New code should use versioned_url_for instead. :param tuple version: The version is always a minimum version in the same major release as there should be no compatibility issues with using a version newer than the one asked for. :returns: The url for the specified version or None if no match. :rtype: str """ data = self.data_for(version, **kwargs) return data['url'] if data else None def versioned_data_for(self, url=None, min_version=None, max_version=None, **kwargs): """Return endpoint data for the service at a url. min_version and max_version can be given either as strings or tuples. :param string url: If url is given, the data will be returned for the endpoint data that has a self link matching the url. :param min_version: The minimum endpoint version that is acceptable. If min_version is given with no max_version it is as if max version is 'latest'. If min_version is 'latest', max_version may only be 'latest' or None. :param max_version: The maximum endpoint version that is acceptable. If min_version is given with no max_version it is as if max version is 'latest'. If min_version is 'latest', max_version may only be 'latest' or None. :returns: the endpoint data for a URL that matches the required version (the format is described in version_data) or None if no match. :rtype: dict """ min_version, max_version = _normalize_version_args( None, min_version, max_version) no_version = not max_version and not min_version version_data = self.version_data(reverse=True, **kwargs) # If we don't have to check a min_version, we can short # circuit anything else if (max_version == (LATEST, LATEST) and (not min_version or min_version == (LATEST, LATEST))): # because we reverse we can just take the first entry return version_data[0] if url: url = url.rstrip('/') + '/' if no_version and not url: # because we reverse we can just take the first entry return version_data[0] # Version data is in order from highest to lowest, so we return # the first matching entry for data in version_data: if url and data['url'] and data['url'].rstrip('/') + '/' == url: return data if _latest_soft_match(min_version, data['version']): return data # Only validate version bounds if versions were specified if min_version and max_version and version_between( min_version, max_version, data['version']): return data # If there is no version requested and we could not find a matching # url in the discovery doc, that means we've got an unversioned # endpoint in the catalog and the user is requesting version data # so that they know what version they got. We can return the first # entry from version_data, because the user hasn't requested anything # different. if no_version and url and len(version_data) > 0: return version_data[0] # We couldn't find a match. return None def versioned_url_for(self, min_version=None, max_version=None, **kwargs): """Get the endpoint url for a version. min_version and max_version can be given either as strings or tuples. :param min_version: The minimum version that is acceptable. If min_version is given with no max_version it is as if max version is 'latest'. :param max_version: The maximum version that is acceptable. If min_version is given with no max_version it is as if max version is 'latest'. :returns: The url for the specified version or None if no match. :rtype: str """ data = self.versioned_data_for(min_version=min_version, max_version=max_version, **kwargs) return data['url'] if data else None class VersionData(dict): """Normalized Version Data about an endpoint.""" def __init__( self, version, url, collection=None, max_microversion=None, min_microversion=None, next_min_version=None, not_before=None, status='CURRENT', raw_status=None): super(VersionData, self).__init__() self['version'] = version self['url'] = url self['collection'] = collection self['max_microversion'] = max_microversion self['min_microversion'] = min_microversion self['next_min_version'] = next_min_version self['not_before'] = not_before self['status'] = status self['raw_status'] = raw_status @property def version(self): """The normalized version of the endpoint.""" return self.get('version') @property def url(self): """The url for the endpoint.""" return self.get('url') @property def collection(self): """The URL for the discovery document. May be None. """ return self.get('collection') @property def min_microversion(self): """The minimum microversion supported by the endpoint. May be None. """ return self.get('min_microversion') @property def max_microversion(self): """The maximum microversion supported by the endpoint. May be None. """ return self.get('max_microversion') @property def status(self): """A canonicalized version of the status. Valid values are CURRENT, SUPPORTED, DEPRECATED and EXPERIMENTAL. """ return self.get('status') @property def raw_status(self): """The status as provided by the server.""" return self.get('raw_status') class EndpointData(object): """Normalized information about a discovered endpoint. Contains url, version, microversion, interface and region information. This is essentially the data contained in the catalog and the version discovery documents about an endpoint that is used to select the endpoint desired by the user. It is returned so that a user can know which qualities a discovered endpoint had, in case their request allowed for a range of possibilities. """ def __init__(self, catalog_url=None, service_url=None, service_type=None, service_name=None, service_id=None, region_name=None, interface=None, endpoint_id=None, raw_endpoint=None, api_version=None, major_version=None, min_microversion=None, max_microversion=None, next_min_version=None, not_before=None, status=None): self.catalog_url = catalog_url self.service_url = service_url self.service_type = service_type self.service_name = service_name self.service_id = service_id self.interface = interface self.region_name = region_name self.endpoint_id = endpoint_id self.raw_endpoint = raw_endpoint self.major_version = major_version self.min_microversion = min_microversion self.max_microversion = max_microversion self.next_min_version = next_min_version self.not_before = not_before self.status = status self._saved_project_id = None self._catalog_matches_version = False self._catalog_matches_exactly = False self._disc = None self.api_version = api_version or _version_from_url(self.url) def __copy__(self): """Return a new EndpointData based on this one.""" new_data = EndpointData( catalog_url=self.catalog_url, service_url=self.service_url, service_type=self.service_type, service_name=self.service_name, service_id=self.service_id, region_name=self.region_name, interface=self.interface, endpoint_id=self.endpoint_id, raw_endpoint=self.raw_endpoint, api_version=self.api_version, major_version=self.major_version, min_microversion=self.min_microversion, max_microversion=self.max_microversion, next_min_version=self.next_min_version, not_before=self.not_before, status=self.status, ) # Save cached discovery object - but we don't want to # actually provide a constructor argument new_data._disc = self._disc new_data._saved_project_id = self._saved_project_id return new_data def __str__(self): """Produce a string like EndpointData{key=val, ...}, for debugging.""" str_attrs = ( 'api_version', 'catalog_url', 'endpoint_id', 'interface', 'major_version', 'max_microversion', 'min_microversion', 'next_min_version', 'not_before', 'raw_endpoint', 'region_name', 'service_id', 'service_name', 'service_type', 'service_url', 'url') return "%s{%s}" % (self.__class__.__name__, ', '.join( ["%s=%s" % (attr, getattr(self, attr)) for attr in str_attrs])) @property def url(self): return self.service_url or self.catalog_url def get_current_versioned_data(self, session, allow=None, cache=None, project_id=None): """Run version discovery on the current endpoint. A simplified version of get_versioned_data, get_current_versioned_data runs discovery but only on the endpoint that has been found already. It can be useful in some workflows where the user wants version information about the endpoint they have. :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :param dict allow: Extra filters to pass when discovering API versions. (optional) :param dict cache: A dict to be used for caching results in addition to caching them on the Session. (optional) :param string project_id: ID of the currently scoped project. Used for removing project_id components of URLs from the catalog. (optional) :returns: A new EndpointData with the requested versioned data. :rtype: :py:class:`keystoneauth1.discover.EndpointData` :raises keystoneauth1.exceptions.discovery.DiscoveryFailure: If the appropriate versioned data could not be discovered. """ min_version, max_version = _normalize_version_args( self.api_version, None, None) return self.get_versioned_data( session=session, allow=allow, cache=cache, allow_version_hack=True, discover_versions=True, min_version=min_version, max_version=max_version) def get_versioned_data(self, session, allow=None, cache=None, allow_version_hack=True, project_id=None, discover_versions=True, min_version=None, max_version=None): """Run version discovery for the service described. Performs Version Discovery and returns a new EndpointData object with information found. min_version and max_version can be given either as strings or tuples. :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :param dict allow: Extra filters to pass when discovering API versions. (optional) :param dict cache: A dict to be used for caching results in addition to caching them on the Session. (optional) :param bool allow_version_hack: Allow keystoneauth to hack up catalog URLS to support older schemes. (optional, default True) :param string project_id: ID of the currently scoped project. Used for removing project_id components of URLs from the catalog. (optional) :param bool discover_versions: Whether to get version metadata from the version discovery document even if it's not neccessary to fulfill the major version request. (optional, defaults to True) :param min_version: The minimum version that is acceptable. If min_version is given with no max_version it is as if max version is 'latest'. :param max_version: The maximum version that is acceptable. If min_version is given with no max_version it is as if max version is 'latest'. :returns: A new EndpointData with the requested versioned data. :rtype: :py:class:`keystoneauth1.discover.EndpointData` :raises keystoneauth1.exceptions.discovery.DiscoveryFailure: If the appropriate versioned data could not be discovered. """ min_version, max_version = _normalize_version_args( None, min_version, max_version) if not allow: allow = {} # This method should always return a new EndpointData new_data = copy.copy(self) new_data._set_version_info( session=session, allow=allow, cache=cache, allow_version_hack=allow_version_hack, project_id=project_id, discover_versions=discover_versions, min_version=min_version, max_version=max_version) return new_data def get_all_version_string_data(self, session, project_id=None): """Return version data for all versions discovery can find. :param string project_id: ID of the currently scoped project. Used for removing project_id components of URLs from the catalog. (optional) :returns: A list of :class:`VersionData` sorted by version number. :rtype: list(VersionData) """ versions = [] for vers_url in self._get_discovery_url_choices(project_id=project_id): try: d = get_discovery(session, vers_url) except Exception as e: # Ignore errors here - we're just searching for one of the # URLs that will give us data. _LOGGER.debug( "Failed attempt at discovery on %s: %s", vers_url, str(e)) continue for version in d.version_string_data(): versions.append(version) break return versions or self._infer_version_data(project_id) def _infer_version_data(self, project_id=None): """Return version data dict for when discovery fails. :param string project_id: ID of the currently scoped project. Used for removing project_id components of URLs from the catalog. (optional) :returns: A list of :class:`VersionData` sorted by version number. :rtype: list(VersionData) """ version = self.api_version if version: version = version_to_string(self.api_version) url = self.url.rstrip("/") if project_id and url.endswith(project_id): url, _ = self.url.rsplit('/', 1) url += "/" return [VersionData(url=url, version=version)] def _set_version_info(self, session, allow=None, cache=None, allow_version_hack=True, project_id=None, discover_versions=False, min_version=None, max_version=None): match_url = None no_version = not max_version and not min_version if no_version and not discover_versions: # NOTE(jamielennox): This may not be the best thing to default to # but is here for backwards compatibility. It may be worth # defaulting to the most recent version. return elif no_version and discover_versions: # We want to run discovery, but we don't want to find different # endpoints than what's in the catalog allow_version_hack = False match_url = self.url if project_id: self.project_id = project_id discovered_data = None # Maybe we've run discovery in the past and have a document that can # satisfy the request without further work if self._disc: discovered_data = self._disc.versioned_data_for( min_version=min_version, max_version=max_version, url=match_url, **allow) if not discovered_data: self._run_discovery( session=session, cache=cache, min_version=min_version, max_version=max_version, project_id=project_id, allow_version_hack=allow_version_hack, discover_versions=discover_versions) if not self._disc: return discovered_data = self._disc.versioned_data_for( min_version=min_version, max_version=max_version, url=match_url, **allow) if not discovered_data: if min_version and not max_version: raise exceptions.DiscoveryFailure( "Minimum version {min_version} was not found".format( min_version=version_to_string(min_version))) elif max_version and not min_version: raise exceptions.DiscoveryFailure( "Maximum version {max_version} was not found".format( max_version=version_to_string(max_version))) elif min_version and max_version: raise exceptions.DiscoveryFailure( "No version found between {min_version}" " and {max_version}".format( min_version=version_to_string(min_version), max_version=version_to_string(max_version))) else: raise exceptions.DiscoveryFailure( "No version data found remotely at all") self.min_microversion = discovered_data['min_microversion'] self.max_microversion = discovered_data['max_microversion'] self.next_min_version = discovered_data['next_min_version'] self.not_before = discovered_data['not_before'] self.api_version = discovered_data['version'] self.status = discovered_data['status'] # TODO(mordred): these next two things should be done by Discover # in versioned_data_for. discovered_url = discovered_data['url'] # NOTE(jamielennox): urljoin allows the url to be relative or even # protocol-less. The additional trailing '/' make urljoin respect # the current path as canonical even if the url doesn't include it. # for example a "v2" path from http://host/admin should resolve as # http://host/admin/v2 where it would otherwise be host/v2. # This has no effect on absolute urls returned from url_for. url = urllib.parse.urljoin(self._disc._url.rstrip('/') + '/', discovered_url) # If we had to pop a project_id from the catalog_url, put it back on if self._saved_project_id: url = urllib.parse.urljoin(url.rstrip('/') + '/', self._saved_project_id) self.service_url = url def _run_discovery(self, session, cache, min_version, max_version, project_id, allow_version_hack, discover_versions): tried = set() for vers_url in self._get_discovery_url_choices( project_id=project_id, allow_version_hack=allow_version_hack, min_version=min_version, max_version=max_version): if self._catalog_matches_exactly and not discover_versions: # The version we started with is correct, and we don't want # new data return if vers_url in tried: continue tried.add(vers_url) try: self._disc = get_discovery( session, vers_url, cache=cache, authenticated=False) break except (exceptions.DiscoveryFailure, exceptions.HttpError, exceptions.ConnectionError): continue if not self._disc: # We couldn't find a version discovery document anywhere. if self._catalog_matches_version: # But - the version in the catalog is fine. self.service_url = self.catalog_url return # NOTE(jamielennox): The logic here is required for backwards # compatibility. By itself it is not ideal. if allow_version_hack: # NOTE(jamielennox): If we can't contact the server we # fall back to just returning the URL from the catalog. This # is backwards compatible behaviour and used when there is no # other choice. Realistically if you have provided a version # you should be able to rely on that version being returned or # the request failing. _LOGGER.warning( 'Failed to contact the endpoint at %s for ' 'discovery. Fallback to using that endpoint as ' 'the base url.', self.url) return else: # NOTE(jamielennox): If you've said no to allow_version_hack # and we can't determine the actual URL this is a failure # because we are specifying that the deployment must be up to # date enough to properly specify a version and keystoneauth # can't deliver. raise exceptions.DiscoveryFailure( "Version requested but version discovery document was not" " found and allow_version_hack was False") def _get_discovery_url_choices( self, project_id=None, allow_version_hack=True, min_version=None, max_version=None): """Find potential locations for version discovery URLs. min_version and max_version are already normalized, so will either be None or a tuple. """ url = urllib.parse.urlparse(self.url.rstrip('/')) url_parts = url.path.split('/') # First, check to see if the catalog url ends with a project id # We need to remove it and save it for later if it does if project_id and url_parts[-1].endswith(project_id): self._saved_project_id = url_parts.pop() elif not project_id: # Peek to see if -2 is a version. If so, -1 is a project_id, # even if we don't know that at this point in the call stack try: normalize_version_number(url_parts[-2]) self._saved_project_id = url_parts.pop() except (IndexError, TypeError): pass catalog_discovery = versioned_discovery = None # Next, check to see if the url indicates a version and if that # version either matches our version request or is within the # range requested. If so, we can start by trying the given url # as it has a high potential for success. try: url_version = normalize_version_number(url_parts[-1]) versioned_discovery = urllib.parse.ParseResult( url.scheme, url.netloc, '/'.join(url_parts), url.params, url.query, url.fragment).geturl() except TypeError: pass else: # `is_between` means version bounds were specified *and* the URL # version is between them. is_between = min_version and max_version and version_between( min_version, max_version, url_version) exact_match = (is_between and max_version and max_version[0] == url_version[0]) high_match = (is_between and max_version and max_version[1] != LATEST and version_match(max_version, url_version)) if exact_match or is_between: self._catalog_matches_version = True self._catalog_matches_exactly = exact_match # The endpoint from the catalog matches the version request # We construct a URL minus any project_id, but we don't # return it just yet. It's a good option, but unless we # have an exact match or match the max requested, we want # to try for an unversioned endpoint first. catalog_discovery = urllib.parse.ParseResult( url.scheme, url.netloc, '/'.join(url_parts), url.params, url.query, url.fragment).geturl().rstrip('/') + '/' # If we found a viable catalog endpoint and it's # an exact match or matches the max, go ahead and give # it a go. if catalog_discovery and (high_match or exact_match): yield catalog_discovery catalog_discovery = None url_parts.pop() if allow_version_hack: # If there were projects or versions in the url they are now gone. # That means we're left with what should be the unversioned url. hacked_url = urllib.parse.ParseResult( url.scheme, url.netloc, '/'.join(url_parts), url.params, url.query, url.fragment).geturl() # Since this is potentially us constructing a base URL from the # versioned URL - we need to make sure it has a trailing /. But # we only want to do that if we have built a new URL - not if # we're using the one from the catalog if hacked_url != self.catalog_url: hacked_url = hacked_url.strip('/') + '/' yield hacked_url # If we have a catalog discovery url, it either means we didn't # return it earlier because it wasn't an exact enough match, or # that we did and it failed. We don't double-request things when # consuming this, so it's safe to return it here in case we didn't # already return it. if catalog_discovery: yield catalog_discovery # NOTE(mordred): For backwards compatibility people might have # added version hacks using the version hack system. The logic # above should handle most cases, so by the time we get here it's # most likely to be a no-op yield self._get_catalog_discover_hack() elif versioned_discovery and self._saved_project_id: # We popped a project_id but are either avoiding version hacks # or we didn't request a version. That means we still want to fetch # the document from the "catalog url" - but the catalog url is has # a project_id suffix so is likely not going to work for us. Try # fetching from the project-less versioned endpoint. yield versioned_discovery # As a final fallthrough case, return the actual unmodified url from # the catalog. yield self.catalog_url def _get_catalog_discover_hack(self): """Apply the catalog hacks and figure out an unversioned endpoint. This function is internal to keystoneauth1. :returns: A url that has been transformed by the regex hacks that match the service_type. """ return _VERSION_HACKS.get_discover_hack(self.service_type, self.url) def get_discovery(session, url, cache=None, authenticated=False): """Return the discovery object for a URL. Check the session and the plugin cache to see if we have already performed discovery on the URL and if so return it, otherwise create a new discovery object, cache it and return it. NOTE: This function is expected to be used by keystoneauth and should not be needed by users part of normal usage. A normal user should use get_endpoint or get_endpoint_data on `keystoneauth.session.Session` or endpoint_filters on `keystoneauth.session.Session` or `keystoneauth.session.Session`. However, should the user need to perform direct discovery for some reason, this function should be used so that the discovery caching is used. :param session: A session object to discover with. :type session: keystoneauth1.session.Session :param str url: The url to lookup. :param dict cache: A dict to be used for caching results, in addition to caching them on the Session. (optional) Defaults to None. :param bool authenticated: Include a token in the discovery call. (optional) Defaults to None, which will use a token if an auth plugin is installed. :raises keystoneauth1.exceptions.discovery.DiscoveryFailure: if for some reason the lookup fails. :raises keystoneauth1.exceptions.http.HttpError: An error from an invalid HTTP response. :returns: A discovery object with the results of looking up that URL. :rtype: :py:class:`keystoneauth1.discover.Discovery` """ # There are between one and three different caches. The user may have # passed one in. There is definitely one on the session, and there is # one on the auth plugin if the Session has an auth plugin. caches = [] # If a cache was passed in, check it first. if cache is not None: caches.append(cache) # If the session has a cache, check it second, since it could have been # provided by the user at Session creation time. if hasattr(session, '_discovery_cache'): caches.append(session._discovery_cache) # Finally check the auth cache associated with the Session. if session.auth and hasattr(session.auth, '_discovery_cache'): caches.append(session.auth._discovery_cache) # https://example.com and https://example.com/ should be treated the same # for caching purposes. parsed_url = urllib.parse.urlparse(url) if parsed_url.path in ('', '/'): url = urllib.parse.ParseResult( parsed_url.scheme, parsed_url.netloc, '', parsed_url.params, parsed_url.query, parsed_url.fragment).geturl() for cache in caches: disc = cache.get(url) if disc: break else: disc = Discover(session, url, authenticated=authenticated) # Whether we get one from fetching or from cache, set it in the # caches. This assures that if we combine sessions and auth plugins # that we don't make unnecessary calls. if disc: for cache in caches: cache[url] = disc return disc class _VersionHacks(object): """A container to abstract the list of version hacks. This could be done as simply a dictionary but is abstracted like this to make for easier testing. """ def __init__(self): self._discovery_data = {} def add_discover_hack(self, service_type, old, new=''): """Add a new hack for a service type. :param str service_type: The service_type in the catalog. :param re.RegexObject old: The pattern to use. :param str new: What to replace the pattern with. """ hacks = self._discovery_data.setdefault(service_type, []) hacks.append((old, new)) def get_discover_hack(self, service_type, url): """Apply the catalog hacks and figure out an unversioned endpoint. :param str service_type: the service_type to look up. :param str url: The original url that came from a service_catalog. :returns: Either the unversioned url or the one from the catalog to try. """ for old, new in self._discovery_data.get(service_type, []): new_string, number_of_subs_made = old.subn(new, url) if number_of_subs_made > 0: return new_string return url _VERSION_HACKS = _VersionHacks() _VERSION_HACKS.add_discover_hack('identity', re.compile('/v2.0/?$'), '/') def add_catalog_discover_hack(service_type, old, new): """Add a version removal rule for a particular service. Originally deployments of OpenStack would contain a versioned endpoint in the catalog for different services. E.g. an identity service might look like ``http://localhost:5000/v2.0``. This is a problem when we want to use a different version like v3.0 as there is no way to tell where it is located. We cannot simply change all service catalogs either so there must be a way to handle the older style of catalog. This function adds a rule for a given service type that if part of the URL matches a given regular expression in *old* then it will be replaced with the *new* value. This will replace all instances of old with new. It should therefore contain a regex anchor. For example the included rule states:: add_catalog_version_hack('identity', re.compile('/v2.0/?$'), '/') so if the catalog retrieves an *identity* URL that ends with /v2.0 or /v2.0/ then it should replace it simply with / to fix the user's catalog. :param str service_type: The service type as defined in the catalog that the rule will apply to. :param re.RegexObject old: The regular expression to search for and replace if found. :param str new: The new string to replace the pattern with. """ _VERSION_HACKS.add_discover_hack(service_type, old, new) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2807944 keystoneauth1-4.4.0/keystoneauth1/exceptions/0000775000175000017500000000000000000000000021333 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/exceptions/__init__.py0000664000175000017500000000212200000000000023441 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.exceptions.auth import * # noqa from keystoneauth1.exceptions.auth_plugins import * # noqa from keystoneauth1.exceptions.base import * # noqa from keystoneauth1.exceptions.catalog import * # noqa from keystoneauth1.exceptions.connection import * # noqa from keystoneauth1.exceptions.discovery import * # noqa from keystoneauth1.exceptions.http import * # noqa from keystoneauth1.exceptions.oidc import * # noqa from keystoneauth1.exceptions.response import * # noqa from keystoneauth1.exceptions.service_providers import * # noqa ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/exceptions/auth.py0000664000175000017500000000246100000000000022651 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import _utils as utils from keystoneauth1.exceptions import base class AuthorizationFailure(base.ClientException): message = "Cannot authorize API client." class MissingAuthMethods(base.ClientException): message = "Not all required auth rules were satisfied" def __init__(self, response): self.response = response self.receipt = response.headers.get("Openstack-Auth-Receipt") body = response.json() self.methods = body['receipt']['methods'] self.required_auth_methods = body['required_auth_methods'] self.expires_at = utils.parse_isotime(body['receipt']['expires_at']) message = "%s: %s" % (self.message, self.required_auth_methods) super(MissingAuthMethods, self).__init__(message) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/exceptions/auth_plugins.py0000664000175000017500000000556400000000000024421 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.exceptions import base __all__ = ('AuthPluginException', 'MissingAuthPlugin', 'NoMatchingPlugin', 'UnsupportedParameters', 'OptionError', 'MissingRequiredOptions') class AuthPluginException(base.ClientException): message = "Unknown error with authentication plugins." class MissingAuthPlugin(AuthPluginException): message = "An authenticated request is required but no plugin available." class NoMatchingPlugin(AuthPluginException): """No auth plugins could be created from the parameters provided. :param str name: The name of the plugin that was attempted to load. .. py:attribute:: name The name of the plugin that was attempted to load. """ def __init__(self, name): self.name = name msg = 'The plugin %s could not be found' % name super(NoMatchingPlugin, self).__init__(msg) class UnsupportedParameters(AuthPluginException): """A parameter that was provided or returned is not supported. :param list(str) names: Names of the unsupported parameters. .. py:attribute:: names Names of the unsupported parameters. """ def __init__(self, names): self.names = names m = 'The following parameters were given that are unsupported: %s' super(UnsupportedParameters, self).__init__(m % ', '.join(self.names)) class OptionError(AuthPluginException): """A requirement of this plugin loader was not met. This error can be raised by a specific plugin loader during the load_from_options stage to indicate a parameter problem that can not be handled by the generic options loader. The intention here is that a plugin can do checks like if a name parameter is provided then a domain parameter must also be provided, but that Opt checking doesn't handle. """ class MissingRequiredOptions(OptionError): """One or more required options were not provided. :param list(keystoneauth1.loading.Opt) options: Missing options. .. py:attribute:: options List of the missing options. """ def __init__(self, options): self.options = options names = ", ".join(o.dest for o in options) m = 'Auth plugin requires parameters which were not given: %s' super(MissingRequiredOptions, self).__init__(m % names) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/exceptions/base.py0000664000175000017500000000153100000000000022617 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. __all__ = ('ClientException',) class ClientException(Exception): """The base exception for everything to do with clients.""" message = "ClientException" def __init__(self, message=None): self.message = message or self.message super(ClientException, self).__init__(self.message) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/exceptions/catalog.py0000664000175000017500000000172300000000000023322 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.exceptions import base __all__ = ('CatalogException', 'EmptyCatalog', 'EndpointNotFound') class CatalogException(base.ClientException): message = "Unknown error with service catalog." class EndpointNotFound(CatalogException): message = "Could not find requested endpoint in Service Catalog." class EmptyCatalog(EndpointNotFound): message = "The service catalog is empty." ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/exceptions/connection.py0000664000175000017500000000304000000000000024041 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.exceptions import base __all__ = ('ConnectionError', 'ConnectTimeout', 'ConnectFailure', 'SSLError', 'RetriableConnectionFailure', 'UnknownConnectionError') class RetriableConnectionFailure(Exception): """A mixin class that implies you can retry the most recent request.""" pass class ConnectionError(base.ClientException): message = "Cannot connect to API service." class ConnectTimeout(ConnectionError, RetriableConnectionFailure): message = "Timed out connecting to service." class ConnectFailure(ConnectionError, RetriableConnectionFailure): message = "Connection failure that may be retried." class SSLError(ConnectionError): message = "An SSL error occurred." class UnknownConnectionError(ConnectionError): """An error was encountered but we don't know what it is.""" def __init__(self, msg, original): super(UnknownConnectionError, self).__init__(msg) self.original = original ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/exceptions/discovery.py0000664000175000017500000000356500000000000023725 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os_service_types from keystoneauth1.exceptions import base _SERVICE_TYPES = os_service_types.ServiceTypes() __all__ = ('DiscoveryFailure', 'ImpliedVersionMismatch', 'ImpliedMinVersionMismatch', 'ImpliedMaxVersionMismatch', 'VersionNotAvailable') class DiscoveryFailure(base.ClientException): message = "Discovery of client versions failed." class VersionNotAvailable(DiscoveryFailure): message = "Discovery failed. Requested version is not available." class ImpliedVersionMismatch(ValueError): label = 'version' def __init__(self, service_type, implied, given): super(ImpliedVersionMismatch, self).__init__( "service_type {service_type} was given which implies" " major API version {implied} but {label} of" " {given} was also given. Please update your code" " to use the official service_type {official_type}.".format( service_type=service_type, implied=str(implied[0]), given=given, label=self.label, official_type=_SERVICE_TYPES.get_service_type(service_type), )) class ImpliedMinVersionMismatch(ImpliedVersionMismatch): label = 'min_version' class ImpliedMaxVersionMismatch(ImpliedVersionMismatch): label = 'max_version' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/exceptions/http.py0000664000175000017500000003012300000000000022663 0ustar00zuulzuul00000000000000# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 Nebula, Inc. # Copyright 2013 Alessio Ababilov # Copyright 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """HTTP Exceptions used by keystoneauth1.""" import inspect import sys from keystoneauth1.exceptions import auth from keystoneauth1.exceptions import base __all__ = ('HttpError', 'HTTPClientError', 'BadRequest', 'Unauthorized', 'PaymentRequired', 'Forbidden', 'NotFound', 'MethodNotAllowed', 'NotAcceptable', 'ProxyAuthenticationRequired', 'RequestTimeout', 'Conflict', 'Gone', 'LengthRequired', 'PreconditionFailed', 'RequestEntityTooLarge', 'RequestUriTooLong', 'UnsupportedMediaType', 'RequestedRangeNotSatisfiable', 'ExpectationFailed', 'UnprocessableEntity', 'HttpServerError', 'InternalServerError', 'HttpNotImplemented', 'BadGateway', 'ServiceUnavailable', 'GatewayTimeout', 'HttpVersionNotSupported', 'from_response') class HttpError(base.ClientException): """The base exception class for all HTTP exceptions.""" http_status = 0 message = "HTTP Error" def __init__(self, message=None, details=None, response=None, request_id=None, url=None, method=None, http_status=None, retry_after=0): self.http_status = http_status or self.http_status self.message = message or self.message self.details = details self.request_id = request_id self.response = response self.url = url self.method = method formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) self.retry_after = retry_after if request_id: formatted_string += " (Request-ID: %s)" % request_id super(HttpError, self).__init__(formatted_string) class HTTPClientError(HttpError): """Client-side HTTP error. Exception for cases in which the client seems to have erred. """ message = "HTTP Client Error" class HttpServerError(HttpError): """Server-side HTTP error. Exception for cases in which the server is aware that it has erred or is incapable of performing the request. """ message = "HTTP Server Error" class BadRequest(HTTPClientError): """HTTP 400 - Bad Request. The request cannot be fulfilled due to bad syntax. """ http_status = 400 message = "Bad Request" class Unauthorized(HTTPClientError): """HTTP 401 - Unauthorized. Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. """ http_status = 401 message = "Unauthorized" class PaymentRequired(HTTPClientError): """HTTP 402 - Payment Required. Reserved for future use. """ http_status = 402 message = "Payment Required" class Forbidden(HTTPClientError): """HTTP 403 - Forbidden. The request was a valid request, but the server is refusing to respond to it. """ http_status = 403 message = "Forbidden" class NotFound(HTTPClientError): """HTTP 404 - Not Found. The requested resource could not be found but may be available again in the future. """ http_status = 404 message = "Not Found" class MethodNotAllowed(HTTPClientError): """HTTP 405 - Method Not Allowed. A request was made of a resource using a request method not supported by that resource. """ http_status = 405 message = "Method Not Allowed" class NotAcceptable(HTTPClientError): """HTTP 406 - Not Acceptable. The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request. """ http_status = 406 message = "Not Acceptable" class ProxyAuthenticationRequired(HTTPClientError): """HTTP 407 - Proxy Authentication Required. The client must first authenticate itself with the proxy. """ http_status = 407 message = "Proxy Authentication Required" class RequestTimeout(HTTPClientError): """HTTP 408 - Request Timeout. The server timed out waiting for the request. """ http_status = 408 message = "Request Timeout" class Conflict(HTTPClientError): """HTTP 409 - Conflict. Indicates that the request could not be processed because of conflict in the request, such as an edit conflict. """ http_status = 409 message = "Conflict" class Gone(HTTPClientError): """HTTP 410 - Gone. Indicates that the resource requested is no longer available and will not be available again. """ http_status = 410 message = "Gone" class LengthRequired(HTTPClientError): """HTTP 411 - Length Required. The request did not specify the length of its content, which is required by the requested resource. """ http_status = 411 message = "Length Required" class PreconditionFailed(HTTPClientError): """HTTP 412 - Precondition Failed. The server does not meet one of the preconditions that the requester put on the request. """ http_status = 412 message = "Precondition Failed" class RequestEntityTooLarge(HTTPClientError): """HTTP 413 - Request Entity Too Large. The request is larger than the server is willing or able to process. """ http_status = 413 message = "Request Entity Too Large" def __init__(self, *args, **kwargs): try: self.retry_after = int(kwargs.pop('retry_after')) except (KeyError, ValueError): self.retry_after = 0 super(RequestEntityTooLarge, self).__init__(*args, **kwargs) class RequestUriTooLong(HTTPClientError): """HTTP 414 - Request-URI Too Long. The URI provided was too long for the server to process. """ http_status = 414 message = "Request-URI Too Long" class UnsupportedMediaType(HTTPClientError): """HTTP 415 - Unsupported Media Type. The request entity has a media type which the server or resource does not support. """ http_status = 415 message = "Unsupported Media Type" class RequestedRangeNotSatisfiable(HTTPClientError): """HTTP 416 - Requested Range Not Satisfiable. The client has asked for a portion of the file, but the server cannot supply that portion. """ http_status = 416 message = "Requested Range Not Satisfiable" class ExpectationFailed(HTTPClientError): """HTTP 417 - Expectation Failed. The server cannot meet the requirements of the Expect request-header field. """ http_status = 417 message = "Expectation Failed" class UnprocessableEntity(HTTPClientError): """HTTP 422 - Unprocessable Entity. The request was well-formed but was unable to be followed due to semantic errors. """ http_status = 422 message = "Unprocessable Entity" class InternalServerError(HttpServerError): """HTTP 500 - Internal Server Error. A generic error message, given when no more specific message is suitable. """ http_status = 500 message = "Internal Server Error" # NotImplemented is a python keyword. class HttpNotImplemented(HttpServerError): """HTTP 501 - Not Implemented. The server either does not recognize the request method, or it lacks the ability to fulfill the request. """ http_status = 501 message = "Not Implemented" class BadGateway(HttpServerError): """HTTP 502 - Bad Gateway. The server was acting as a gateway or proxy and received an invalid response from the upstream server. """ http_status = 502 message = "Bad Gateway" class ServiceUnavailable(HttpServerError): """HTTP 503 - Service Unavailable. The server is currently unavailable. """ http_status = 503 message = "Service Unavailable" class GatewayTimeout(HttpServerError): """HTTP 504 - Gateway Timeout. The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. """ http_status = 504 message = "Gateway Timeout" class HttpVersionNotSupported(HttpServerError): """HTTP 505 - HttpVersion Not Supported. The server does not support the HTTP protocol version used in the request. """ http_status = 505 message = "HTTP Version Not Supported" # _code_map contains all the classes that have http_status attribute. _code_map = dict( (getattr(obj, 'http_status', None), obj) for name, obj in vars(sys.modules[__name__]).items() if inspect.isclass(obj) and getattr(obj, 'http_status', False) ) def from_response(response, method, url): """Return an instance of :class:`HttpError` or subclass based on response. :param response: instance of `requests.Response` class :param method: HTTP method used for request :param url: URL used for request """ req_id = response.headers.get("x-openstack-request-id") kwargs = { "http_status": response.status_code, "response": response, "method": method, "url": url, "request_id": req_id, } if "retry-after" in response.headers: kwargs["retry_after"] = response.headers["retry-after"] content_type = response.headers.get("Content-Type", "") if content_type.startswith("application/json"): try: body = response.json() except ValueError: pass else: if isinstance(body, dict) and isinstance(body.get("error"), dict): error = body["error"] kwargs["message"] = error.get("message") kwargs["details"] = error.get("details") elif (isinstance(body, dict) and isinstance(body.get("errors"), list)): # if the error response follows the API SIG guidelines, it # will return a list of errors. in this case, only the first # error is shown, but if there are multiple the user will be # alerted to that fact. errors = body["errors"] if len(errors) == 0: # just in case we get an empty array kwargs["message"] = None kwargs["details"] = None else: if len(errors) > 1: # if there is more than one error, let the user know # that multiple were seen. msg_hdr = ("Multiple error responses, " "showing first only: ") else: msg_hdr = "" kwargs["message"] = "{}{}".format(msg_hdr, errors[0].get("title")) kwargs["details"] = errors[0].get("detail") else: kwargs["message"] = "Unrecognized schema in response body." elif content_type.startswith("text/"): kwargs["details"] = response.text # we check explicity for 401 in case of auth receipts if (response.status_code == 401 and "Openstack-Auth-Receipt" in response.headers): return auth.MissingAuthMethods(response) try: cls = _code_map[response.status_code] except KeyError: if 500 <= response.status_code < 600: cls = HttpServerError elif 400 <= response.status_code < 500: cls = HTTPClientError else: cls = HttpError return cls(**kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/exceptions/oidc.py0000664000175000017500000000317100000000000022625 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.exceptions import auth_plugins __all__ = ( 'InvalidDiscoveryEndpoint', 'InvalidOidcDiscoveryDocument', 'OidcAccessTokenEndpointNotFound', 'OidcAuthorizationEndpointNotFound', 'OidcGrantTypeMissmatch', 'OidcPluginNotSupported', ) class InvalidDiscoveryEndpoint(auth_plugins.AuthPluginException): message = "OpenID Connect Discovery Document endpoint not set.""" class InvalidOidcDiscoveryDocument(auth_plugins.AuthPluginException): message = "OpenID Connect Discovery Document is not valid JSON.""" class OidcAccessTokenEndpointNotFound(auth_plugins.AuthPluginException): message = "OpenID Connect access token endpoint not provided." class OidcAuthorizationEndpointNotFound(auth_plugins.AuthPluginException): message = "OpenID Connect authorization endpoint not provided." class OidcGrantTypeMissmatch(auth_plugins.AuthPluginException): message = "Missmatch between OpenID Connect plugin and grant_type argument" class OidcPluginNotSupported(auth_plugins.AuthPluginException): message = "OpenID Connect grant type not supported by provider." ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/exceptions/response.py0000664000175000017500000000147700000000000023554 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.exceptions import base __all__ = ('InvalidResponse',) class InvalidResponse(base.ClientException): message = "Invalid response from server." def __init__(self, response): super(InvalidResponse, self).__init__() self.response = response ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/exceptions/service_providers.py0000664000175000017500000000163600000000000025450 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.exceptions import base __all__ = ('ServiceProviderNotFound',) class ServiceProviderNotFound(base.ClientException): """A Service Provider cannot be found.""" def __init__(self, sp_id): self.sp_id = sp_id msg = 'The Service Provider %(sp)s could not be found' % {'sp': sp_id} super(ServiceProviderNotFound, self).__init__(msg) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2807944 keystoneauth1-4.4.0/keystoneauth1/extras/0000775000175000017500000000000000000000000020460 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/extras/__init__.py0000664000175000017500000000154400000000000022575 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # NOTE(jamielennox): This directory is designed to reflect the dependency # extras in the setup.cfg file. If you create an additional dependency section # like 'kerberos' in the setup.cfg it is expected that there be a kerberos # package here that can be imported. # # e.g. from keystoneauth1.extras import kerberos pass ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2847943 keystoneauth1-4.4.0/keystoneauth1/extras/_saml2/0000775000175000017500000000000000000000000021635 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/extras/_saml2/__init__.py0000664000175000017500000000142700000000000023752 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.extras._saml2 import v3 _V3_SAML2_AVAILABLE = v3._SAML2_AVAILABLE _V3_ADFS_AVAILABLE = v3._ADFS_AVAILABLE V3Saml2Password = v3.Saml2Password V3ADFSPassword = v3.ADFSPassword __all__ = ('V3Saml2Password', 'V3ADFSPassword') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/extras/_saml2/_loading.py0000664000175000017500000000501200000000000023761 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.extras import _saml2 from keystoneauth1 import loading class Saml2Password(loading.BaseFederationLoader): @property def plugin_class(self): return _saml2.V3Saml2Password @property def available(self): return _saml2._V3_SAML2_AVAILABLE def get_options(self): options = super(Saml2Password, self).get_options() options.extend([ loading.Opt('identity-provider-url', required=True, help=('An Identity Provider URL, where the SAML2 ' 'authentication request will be sent.')), loading.Opt('username', help='Username', required=True), loading.Opt('password', secret=True, help='Password', required=True) ]) return options class ADFSPassword(loading.BaseFederationLoader): @property def plugin_class(self): return _saml2.V3ADFSPassword @property def available(self): return _saml2._V3_ADFS_AVAILABLE def get_options(self): options = super(ADFSPassword, self).get_options() options.extend([ loading.Opt('identity-provider-url', required=True, help=('An Identity Provider URL, where the SAML ' 'authentication request will be sent.')), loading.Opt('service-provider-endpoint', required=True, help="Service Provider's Endpoint"), loading.Opt('service-provider-entity-id', required=True, help="Service Provider's SAML Entity ID"), loading.Opt('username', help='Username', required=True), loading.Opt('password', secret=True, required=True, help='Password') ]) return options ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2847943 keystoneauth1-4.4.0/keystoneauth1/extras/_saml2/v3/0000775000175000017500000000000000000000000022165 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/extras/_saml2/v3/__init__.py0000664000175000017500000000165000000000000024300 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.extras._saml2.v3 import adfs from keystoneauth1.extras._saml2.v3 import base from keystoneauth1.extras._saml2.v3 import saml2 _SAML2_AVAILABLE = base.etree is not None and saml2.etree is not None _ADFS_AVAILABLE = base.etree is not None and adfs.etree is not None Saml2Password = saml2.Password ADFSPassword = adfs.Password __all__ = ('Saml2Password', 'ADFSPassword') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/extras/_saml2/v3/adfs.py0000664000175000017500000004440500000000000023463 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime import uuid try: from lxml import etree except ImportError: etree = None from six.moves import urllib from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1.extras._saml2.v3 import base class Password(base.BaseSAMLPlugin): """Authentication plugin for Microsoft ADFS2.0 IdPs.""" DEFAULT_ADFS_TOKEN_EXPIRATION = 120 HEADER_SOAP = {"Content-Type": "application/soap+xml; charset=utf-8"} HEADER_X_FORM = {"Content-Type": "application/x-www-form-urlencoded"} NAMESPACES = { 's': 'http://www.w3.org/2003/05/soap-envelope', 'a': 'http://www.w3.org/2005/08/addressing', 'u': ('http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-utility-1.0.xsd') } ADFS_TOKEN_NAMESPACES = { 's': 'http://www.w3.org/2003/05/soap-envelope', 't': 'http://docs.oasis-open.org/ws-sx/ws-trust/200512' } ADFS_ASSERTION_XPATH = ('/s:Envelope/s:Body' '/t:RequestSecurityTokenResponseCollection' '/t:RequestSecurityTokenResponse') def __init__(self, auth_url, identity_provider, identity_provider_url, service_provider_endpoint, username, password, protocol, service_provider_entity_id=None, **kwargs): """Constructor for ``ADFSPassword``. :param auth_url: URL of the Identity Service :type auth_url: string :param identity_provider: name of the Identity Provider the client will authenticate against. This parameter will be used to build a dynamic URL used to obtain unscoped OpenStack token. :type identity_provider: string :param identity_provider_url: An Identity Provider URL, where the SAML2 authentication request will be sent. :type identity_provider_url: string :param service_provider_endpoint: Endpoint where an assertion is being sent, for instance: ``https://host.domain/Shibboleth.sso/ADFS`` :type service_provider_endpoint: string :param service_provider_entity_id: Service Provider SAML Entity ID :type service_provider_entity_id: string :param username: User's login :type username: string :param password: User's password :type password: string """ super(Password, self).__init__( auth_url=auth_url, identity_provider=identity_provider, identity_provider_url=identity_provider_url, username=username, password=password, protocol=protocol, **kwargs) self.service_provider_endpoint = service_provider_endpoint self.service_provider_entity_id = service_provider_entity_id def _cookies(self, session): """Check if cookie jar is not empty. keystoneauth1.session.Session object doesn't have a cookies attribute. We should then try fetching cookies from the underlying requests.Session object. If that fails too, there is something wrong and let Python raise the AttributeError. :param session :returns: True if cookie jar is nonempty, False otherwise :raises AttributeError: in case cookies are not find anywhere """ try: return bool(session.cookies) except AttributeError: pass return bool(session.session.cookies) def _token_dates(self, fmt='%Y-%m-%dT%H:%M:%S.%fZ'): """Calculate created and expires datetime objects. The method is going to be used for building ADFS Request Security Token message. Time interval between ``created`` and ``expires`` dates is now static and equals to 120 seconds. ADFS security tokens should not be live too long, as currently ``keystoneauth1`` doesn't have mechanisms for reusing such tokens (every time ADFS authn method is called, keystoneauth1 will login with the ADFS instance). :param fmt: Datetime format for specifying string format of a date. It should not be changed if the method is going to be used for building the ADFS security token request. :type fmt: string """ date_created = datetime.datetime.utcnow() date_expires = date_created + datetime.timedelta( seconds=self.DEFAULT_ADFS_TOKEN_EXPIRATION) return [_time.strftime(fmt) for _time in (date_created, date_expires)] def _prepare_adfs_request(self): """Build the ADFS Request Security Token SOAP message. Some values like username or password are inserted in the request. """ WSS_SECURITY_NAMESPACE = { 'o': ('http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-secext-1.0.xsd') } TRUST_NAMESPACE = { 'trust': 'http://docs.oasis-open.org/ws-sx/ws-trust/200512' } WSP_NAMESPACE = { 'wsp': 'http://schemas.xmlsoap.org/ws/2004/09/policy' } WSA_NAMESPACE = { 'wsa': 'http://www.w3.org/2005/08/addressing' } root = etree.Element( '{http://www.w3.org/2003/05/soap-envelope}Envelope', nsmap=self.NAMESPACES) header = etree.SubElement( root, '{http://www.w3.org/2003/05/soap-envelope}Header') action = etree.SubElement( header, "{http://www.w3.org/2005/08/addressing}Action") action.set( "{http://www.w3.org/2003/05/soap-envelope}mustUnderstand", "1") action.text = ('http://docs.oasis-open.org/ws-sx/ws-trust/200512' '/RST/Issue') messageID = etree.SubElement( header, '{http://www.w3.org/2005/08/addressing}MessageID') messageID.text = 'urn:uuid:' + uuid.uuid4().hex replyID = etree.SubElement( header, '{http://www.w3.org/2005/08/addressing}ReplyTo') address = etree.SubElement( replyID, '{http://www.w3.org/2005/08/addressing}Address') address.text = 'http://www.w3.org/2005/08/addressing/anonymous' to = etree.SubElement( header, '{http://www.w3.org/2005/08/addressing}To') to.set("{http://www.w3.org/2003/05/soap-envelope}mustUnderstand", "1") security = etree.SubElement( header, '{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-secext-1.0.xsd}Security', nsmap=WSS_SECURITY_NAMESPACE) security.set( "{http://www.w3.org/2003/05/soap-envelope}mustUnderstand", "1") timestamp = etree.SubElement( security, ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-utility-1.0.xsd}Timestamp')) timestamp.set( ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-utility-1.0.xsd}Id'), '_0') created = etree.SubElement( timestamp, ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-utility-1.0.xsd}Created')) expires = etree.SubElement( timestamp, ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-utility-1.0.xsd}Expires')) created.text, expires.text = self._token_dates() usernametoken = etree.SubElement( security, '{http://docs.oasis-open.org/wss/2004/01/oasis-200401-' 'wss-wssecurity-secext-1.0.xsd}UsernameToken') usernametoken.set( ('{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-' 'wssecurity-utility-1.0.xsd}u'), "uuid-%s-1" % uuid.uuid4().hex) username = etree.SubElement( usernametoken, ('{http://docs.oasis-open.org/wss/2004/01/oasis-' '200401-wss-wssecurity-secext-1.0.xsd}Username')) password = etree.SubElement( usernametoken, ('{http://docs.oasis-open.org/wss/2004/01/oasis-' '200401-wss-wssecurity-secext-1.0.xsd}Password'), Type=('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-' 'username-token-profile-1.0#PasswordText')) body = etree.SubElement( root, "{http://www.w3.org/2003/05/soap-envelope}Body") request_security_token = etree.SubElement( body, ('{http://docs.oasis-open.org/ws-sx/ws-trust/200512}' 'RequestSecurityToken'), nsmap=TRUST_NAMESPACE) applies_to = etree.SubElement( request_security_token, '{http://schemas.xmlsoap.org/ws/2004/09/policy}AppliesTo', nsmap=WSP_NAMESPACE) endpoint_reference = etree.SubElement( applies_to, '{http://www.w3.org/2005/08/addressing}EndpointReference', nsmap=WSA_NAMESPACE) wsa_address = etree.SubElement( endpoint_reference, '{http://www.w3.org/2005/08/addressing}Address') keytype = etree.SubElement( request_security_token, '{http://docs.oasis-open.org/ws-sx/ws-trust/200512}KeyType') keytype.text = ('http://docs.oasis-open.org/ws-sx/' 'ws-trust/200512/Bearer') request_type = etree.SubElement( request_security_token, '{http://docs.oasis-open.org/ws-sx/ws-trust/200512}RequestType') request_type.text = ('http://docs.oasis-open.org/ws-sx/' 'ws-trust/200512/Issue') token_type = etree.SubElement( request_security_token, '{http://docs.oasis-open.org/ws-sx/ws-trust/200512}TokenType') token_type.text = 'urn:oasis:names:tc:SAML:1.0:assertion' # After constructing the request, let's plug in some values username.text = self.username password.text = self.password to.text = self.identity_provider_url wsa_address.text = (self.service_provider_entity_id or self.service_provider_endpoint) self.prepared_request = root def _get_adfs_security_token(self, session): """Send ADFS Security token to the ADFS server. Store the result in the instance attribute and raise an exception in case the response is not valid XML data. If a user cannot authenticate due to providing bad credentials, the ADFS2.0 server will return a HTTP 500 response and a XML Fault message. If ``exceptions.InternalServerError`` is caught, the method tries to parse the XML response. If parsing is unsuccessful, an ``exceptions.AuthorizationFailure`` is raised with a reason from the XML fault. Otherwise an original ``exceptions.InternalServerError`` is re-raised. :param session : a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :raises keystoneauth1.exceptions.AuthorizationFailure: when HTTP response from the ADFS server is not a valid XML ADFS security token. :raises keystoneauth1.exceptions.InternalServerError: If response status code is HTTP 500 and the response XML cannot be recognized. """ def _get_failure(e): xpath = '/s:Envelope/s:Body/s:Fault/s:Code/s:Subcode/s:Value' content = e.response.content try: obj = self.str_to_xml(content).xpath( xpath, namespaces=self.NAMESPACES) obj = self._first(obj) return obj.text # NOTE(marek-denis): etree.Element.xpath() doesn't raise an # exception, it just returns an empty list. In that case, _first() # will raise IndexError and we should treat it as an indication XML # is not valid. exceptions.AuthorizationFailure can be raised from # str_to_xml(), however since server returned HTTP 500 we should # re-raise exceptions.InternalServerError. except (IndexError, exceptions.AuthorizationFailure): raise e request_security_token = self.xml_to_str(self.prepared_request) try: response = session.post( url=self.identity_provider_url, headers=self.HEADER_SOAP, data=request_security_token, authenticated=False) except exceptions.InternalServerError as e: reason = _get_failure(e) raise exceptions.AuthorizationFailure(reason) msg = ('Error parsing XML returned from ' 'the ADFS Identity Provider, reason: %s') self.adfs_token = self.str_to_xml(response.content, msg) def _prepare_sp_request(self): """Prepare ADFS Security Token to be sent to the Service Provider. The method works as follows: * Extract SAML2 assertion from the ADFS Security Token. * Replace namespaces * urlencode assertion * concatenate static string with the encoded assertion """ assertion = self.adfs_token.xpath( self.ADFS_ASSERTION_XPATH, namespaces=self.ADFS_TOKEN_NAMESPACES) assertion = self._first(assertion) assertion = self.xml_to_str(assertion) # TODO(marek-denis): Ideally no string replacement should occur. # Unfortunately lxml doesn't allow for namespaces changing in-place and # probably the only solution good for now is to build the assertion # from scratch and reuse values from the adfs security token. assertion = assertion.replace( b'http://docs.oasis-open.org/ws-sx/ws-trust/200512', b'http://schemas.xmlsoap.org/ws/2005/02/trust') encoded_assertion = urllib.parse.quote(assertion) self.encoded_assertion = 'wa=wsignin1.0&wresult=' + encoded_assertion def _send_assertion_to_service_provider(self, session): """Send prepared assertion to a service provider. As the assertion doesn't contain a protected resource, the value from the ``location`` header is not valid and we should not let the Session object get redirected there. The aim of this call is to get a cookie in the response which is required for entering a protected endpoint. :param session : a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :raises: Corresponding HTTP error exception """ session.post( url=self.service_provider_endpoint, data=self.encoded_assertion, headers=self.HEADER_X_FORM, redirect=False, authenticated=False) def _access_service_provider(self, session): """Access protected endpoint and fetch unscoped token. After federated authentication workflow a protected endpoint should be accessible with the session object. The access is granted basing on the cookies stored within the session object. If, for some reason no cookies are present (quantity test) it means something went wrong and user will not be able to fetch an unscoped token. In that case an ``exceptions.AuthorizationFailure` exception is raised and no HTTP call is even made. :param session : a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :raises keystoneauth1.exceptions.AuthorizationFailure: in case session object has empty cookie jar. """ if self._cookies(session) is False: raise exceptions.AuthorizationFailure( "Session object doesn't contain a cookie, therefore you are " "not allowed to enter the Identity Provider's protected area.") self.authenticated_response = session.get(self.federated_token_url, authenticated=False) def get_unscoped_auth_ref(self, session, *kwargs): """Retrieve unscoped token after authentcation with ADFS server. This is a multistep process: * Prepare ADFS Request Securty Token - build an etree.XML object filling certain attributes with proper user credentials, created/expires dates (ticket is be valid for 120 seconds as currently we don't handle reusing ADFS issued security tokens). * Send ADFS Security token to the ADFS server. Step handled by * Receive and parse security token, extract actual SAML assertion and prepare a request addressed for the Service Provider endpoint. This also includes changing namespaces in the XML document. Step handled by ``ADFSPassword._prepare_sp_request()`` method. * Send prepared assertion to the Service Provider endpoint. Usually the server will respond with HTTP 301 code which should be ignored as the 'location' header doesn't contain protected area. The goal of this operation is fetching the session cookie which later allows for accessing protected URL endpoints. Step handed by ``ADFSPassword._send_assertion_to_service_provider()`` method. * Once the session cookie is issued, the protected endpoint can be accessed and an unscoped token can be retrieved. Step handled by ``ADFSPassword._access_service_provider()`` method. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :returns: AccessInfo :rtype: :py:class:`keystoneauth1.access.AccessInfo` """ self._prepare_adfs_request() self._get_adfs_security_token(session) self._prepare_sp_request() self._send_assertion_to_service_provider(session) self._access_service_provider(session) return access.create(resp=self.authenticated_response) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/extras/_saml2/v3/base.py0000664000175000017500000000654600000000000023464 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. try: from lxml import etree except ImportError: etree = None from keystoneauth1 import exceptions from keystoneauth1.identity import v3 class _Saml2TokenAuthMethod(v3.AuthMethod): _method_parameters = [] def get_auth_data(self, session, auth, headers, **kwargs): raise exceptions.MethodNotImplemented('This method should never ' 'be called') class BaseSAMLPlugin(v3.FederationBaseAuth): HTTP_MOVED_TEMPORARILY = 302 HTTP_SEE_OTHER = 303 _auth_method_class = _Saml2TokenAuthMethod def __init__(self, auth_url, identity_provider, identity_provider_url, username, password, protocol, **kwargs): """Class constructor accepting following parameters. :param auth_url: URL of the Identity Service :type auth_url: string :param identity_provider: Name of the Identity Provider the client will authenticate against. This parameter will be used to build a dynamic URL used to obtain unscoped OpenStack token. :type identity_provider: string :param identity_provider_url: An Identity Provider URL, where the SAML2 auhentication request will be sent. :type identity_provider_url: string :param username: User's login :type username: string :param password: User's password :type password: string :param protocol: Protocol to be used for the authentication. The name must be equal to one configured at the keystone sp side. This value is used for building dynamic authentication URL. Typical value would be: saml2 :type protocol: string """ super(BaseSAMLPlugin, self).__init__( auth_url=auth_url, identity_provider=identity_provider, protocol=protocol, **kwargs) self.identity_provider_url = identity_provider_url self.username = username self.password = password @staticmethod def _first(_list): if len(_list) != 1: raise IndexError('Only single element list is acceptable') return _list[0] @staticmethod def str_to_xml(content, msg=None, include_exc=True): try: return etree.XML(content) except etree.XMLSyntaxError as e: if not msg: msg = str(e) else: msg = msg % e if include_exc else msg raise exceptions.AuthorizationFailure(msg) @staticmethod def xml_to_str(content, **kwargs): return etree.tostring(content, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/extras/_saml2/v3/saml2.py0000664000175000017500000002554700000000000023572 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc try: from lxml import etree except ImportError: etree = None import requests import requests.auth from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1.identity import v3 _PAOS_NAMESPACE = 'urn:liberty:paos:2003-08' _ECP_NAMESPACE = 'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp' _PAOS_HEADER = 'application/vnd.paos+xml' _PAOS_VER = 'ver="%s";"%s"' % (_PAOS_NAMESPACE, _ECP_NAMESPACE) _XML_NAMESPACES = { 'ecp': _ECP_NAMESPACE, 'S': 'http://schemas.xmlsoap.org/soap/envelope/', 'paos': _PAOS_NAMESPACE, } _XBASE = '/S:Envelope/S:Header/' _XPATH_SP_RELAY_STATE = '//ecp:RelayState' _XPATH_SP_CONSUMER_URL = _XBASE + 'paos:Request/@responseConsumerURL' _XPATH_IDP_CONSUMER_URL = _XBASE + 'ecp:Response/@AssertionConsumerServiceURL' _SOAP_FAULT = """ S:Server responseConsumerURL from SP and assertionConsumerServiceURL from IdP do not match """ class SamlException(Exception): """Base SAML plugin exception.""" class InvalidResponse(SamlException): """Invalid Response from SAML authentication.""" class ConsumerMismatch(SamlException): """The SP and IDP consumers do not match.""" def _response_xml(response, name): try: return etree.XML(response.content) except etree.XMLSyntaxError as e: msg = 'SAML2: Error parsing XML returned from %s: %s' % (name, e) raise InvalidResponse(msg) def _str_from_xml(xml, path): li = xml.xpath(path, namespaces=_XML_NAMESPACES) if len(li) != 1: raise IndexError('%s should provide a single element list' % path) return li[0] class _SamlAuth(requests.auth.AuthBase): """A generic SAML ECP plugin for requests. This is a multi-step process including multiple HTTP requests. Authentication consists of: * HTTP GET request to the Service Provider. It's crucial to include HTTP headers indicating we are expecting SOAP message in return. Service Provider should respond with a SOAP message. * HTTP POST request to the external Identity Provider service with ECP extension enabled. The content sent is a header removed SOAP message returned from the Service Provider. It's also worth noting that ECP extension to the SAML2 doesn't define authentication method. The most popular is HttpBasicAuth with just user and password. Other possibilities could be X509 certificates or Kerberos. Upon successful authentication the user should receive a SAML2 assertion. * HTTP POST request again to the Service Provider. The body of the request includes SAML2 assertion issued by a trusted Identity Provider. The request should be sent to the Service Provider consumer url specified in the SAML2 assertion. Providing the authentication was successful and both Service Provider and Identity Providers are trusted to each other, the Service Provider will issue an unscoped token with a list of groups the federated user is a member of. """ def __init__(self, identity_provider_url, requests_auth): super(_SamlAuth, self).__init__() self.identity_provider_url = identity_provider_url self.requests_auth = requests_auth def __call__(self, request): try: accept = request.headers['Accept'] except KeyError: request.headers['Accept'] = _PAOS_HEADER else: request.headers['Accept'] = ','.join([accept, _PAOS_HEADER]) request.headers['PAOS'] = _PAOS_VER request.register_hook('response', self._handle_response) return request def _handle_response(self, response, **kwargs): if (response.status_code == 200 and response.headers.get('Content-Type') == _PAOS_HEADER): response = self._ecp_retry(response, **kwargs) return response def _ecp_retry(self, sp_response, **kwargs): history = [sp_response] def send(*send_args, **send_kwargs): req = requests.Request(*send_args, **send_kwargs) return sp_response.connection.send(req.prepare(), **kwargs) authn_request = _response_xml(sp_response, 'Service Provider') relay_state = _str_from_xml(authn_request, _XPATH_SP_RELAY_STATE) sp_consumer_url = _str_from_xml(authn_request, _XPATH_SP_CONSUMER_URL) authn_request.remove(authn_request[0]) idp_response = send('POST', self.identity_provider_url, headers={'Content-type': 'text/xml'}, data=etree.tostring(authn_request), auth=self.requests_auth) history.append(idp_response) authn_response = _response_xml(idp_response, 'Identity Provider') idp_consumer_url = _str_from_xml(authn_response, _XPATH_IDP_CONSUMER_URL) if sp_consumer_url != idp_consumer_url: # send fault message to the SP, discard the response send('POST', sp_consumer_url, data=_SOAP_FAULT, headers={'Content-Type': _PAOS_HEADER}) # prepare error message and raise an exception. msg = ('Consumer URLs from Service Provider %(service_provider)s ' '%(sp_consumer_url)s and Identity Provider ' '%(identity_provider)s %(idp_consumer_url)s are not equal') msg = msg % { 'service_provider': sp_response.request.url, 'sp_consumer_url': sp_consumer_url, 'identity_provider': self.identity_provider_url, 'idp_consumer_url': idp_consumer_url } raise ConsumerMismatch(msg) authn_response[0][0] = relay_state # idp_consumer_url is the URL on the SP that handles the ECP body # returned and creates an authenticated session. final_resp = send('POST', idp_consumer_url, headers={'Content-Type': _PAOS_HEADER}, cookies=idp_response.cookies, data=etree.tostring(authn_response)) history.append(final_resp) # the SP should then redirect us back to the original URL to retry the # original request. if final_resp.status_code in (requests.codes.found, requests.codes.other): # Consume content and release the original connection # to allow our new request to reuse the same one. sp_response.content sp_response.raw.release_conn() req = sp_response.request.copy() req.url = final_resp.headers['location'] req.prepare_cookies(final_resp.cookies) final_resp = sp_response.connection.send(req, **kwargs) history.append(final_resp) final_resp.history.extend(history) return final_resp class _FederatedSaml(v3.FederationBaseAuth): def __init__(self, auth_url, identity_provider, protocol, identity_provider_url, **kwargs): super(_FederatedSaml, self).__init__(auth_url, identity_provider, protocol, **kwargs) self.identity_provider_url = identity_provider_url @abc.abstractmethod def get_requests_auth(self): raise NotImplementedError() def get_unscoped_auth_ref(self, session, **kwargs): method = self.get_requests_auth() auth = _SamlAuth(self.identity_provider_url, method) try: resp = session.get(self.federated_token_url, requests_auth=auth, authenticated=False) except SamlException as e: raise exceptions.AuthorizationFailure(str(e)) return access.create(resp=resp) class Password(_FederatedSaml): r"""Implement authentication plugin for SAML2 protocol. ECP stands for `Enhanced Client or Proxy` and is a SAML2 extension for federated authentication where a transportation layer consists of HTTP protocol and XML SOAP messages. `Read for more information `_ on ECP. Reference the `SAML2 ECP specification `_. Currently only HTTPBasicAuth mechanism is available for the IdP authenication. :param auth_url: URL of the Identity Service :type auth_url: string :param identity_provider: name of the Identity Provider the client will authenticate against. This parameter will be used to build a dynamic URL used to obtain unscoped OpenStack token. :type identity_provider: string :param identity_provider_url: An Identity Provider URL, where the SAML2 authn request will be sent. :type identity_provider_url: string :param username: User's login :type username: string :param password: User's password :type password: string :param protocol: Protocol to be used for the authentication. The name must be equal to one configured at the keystone sp side. This value is used for building dynamic authentication URL. Typical value would be: saml2 :type protocol: string """ def __init__(self, auth_url, identity_provider, protocol, identity_provider_url, username, password, **kwargs): super(Password, self).__init__(auth_url, identity_provider, protocol, identity_provider_url, **kwargs) self.username = username self.password = password def get_requests_auth(self): return requests.auth.HTTPBasicAuth(self.username, self.password) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2847943 keystoneauth1-4.4.0/keystoneauth1/extras/kerberos/0000775000175000017500000000000000000000000022274 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/extras/kerberos/__init__.py0000664000175000017500000000635200000000000024413 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Kerberos authentication plugins. .. warning:: This module requires installation of an extra package (`requests_kerberos`) not installed by default. Without the extra package an import error will occur. The extra package can be installed using:: $ pip install keystoneauth1[kerberos] """ try: import requests_kerberos except ImportError: requests_kerberos = None from keystoneauth1 import access from keystoneauth1.identity import v3 from keystoneauth1.identity.v3 import federation def _mutual_auth(value): if value is None: return requests_kerberos.OPTIONAL return { 'required': requests_kerberos.REQUIRED, 'optional': requests_kerberos.OPTIONAL, 'disabled': requests_kerberos.DISABLED, }.get(value.lower(), requests_kerberos.OPTIONAL) def _requests_auth(mutual_authentication): return requests_kerberos.HTTPKerberosAuth( mutual_authentication=_mutual_auth(mutual_authentication)) def _dependency_check(): if requests_kerberos is None: raise ImportError(""" Using the kerberos authentication plugin requires installation of additional packages. These can be installed with:: $ pip install keystoneauth1[kerberos] """) class KerberosMethod(v3.AuthMethod): _method_parameters = ['mutual_auth'] def __init__(self, *args, **kwargs): _dependency_check() super(KerberosMethod, self).__init__(*args, **kwargs) def get_auth_data(self, session, auth, headers, request_kwargs, **kwargs): # NOTE(jamielennox): request_kwargs is passed as a kwarg however it is # required and always present when called from keystoneclient. request_kwargs['requests_auth'] = _requests_auth(self.mutual_auth) return 'kerberos', {} class Kerberos(v3.AuthConstructor): _auth_method_class = KerberosMethod class MappedKerberos(federation.FederationBaseAuth): """Authenticate using Kerberos via the keystone federation mechanisms. This uses the OS-FEDERATION extension to gain an unscoped token and then use the standard keystone auth process to scope that to any given project. """ def __init__(self, auth_url, identity_provider, protocol, mutual_auth=None, **kwargs): _dependency_check() self.mutual_auth = mutual_auth super(MappedKerberos, self).__init__(auth_url, identity_provider, protocol, **kwargs) def get_unscoped_auth_ref(self, session, **kwargs): resp = session.get(self.federated_token_url, requests_auth=_requests_auth(self.mutual_auth), authenticated=False) return access.create(body=resp.json(), resp=resp) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/extras/kerberos/_loading.py0000664000175000017500000000542700000000000024432 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import exceptions from keystoneauth1.extras import kerberos from keystoneauth1 import loading class Kerberos(loading.BaseV3Loader): @property def plugin_class(self): return kerberos.Kerberos @property def available(self): return kerberos.requests_kerberos is not None def get_options(self): options = super(Kerberos, self).get_options() options.extend([ loading.Opt('mutual-auth', required=False, default='optional', help='Configures Kerberos Mutual Authentication'), ]) return options def load_from_options(self, **kwargs): if kwargs.get('mutual_auth'): value = kwargs.get('mutual_auth') if not (value.lower() in ['required', 'optional', 'disabled']): m = ('You need to provide a valid value for kerberos mutual ' 'authentication. It can be one of the following: ' '(required, optional, disabled)') raise exceptions.OptionError(m) return super(Kerberos, self).load_from_options(**kwargs) class MappedKerberos(loading.BaseFederationLoader): @property def plugin_class(self): return kerberos.MappedKerberos @property def available(self): return kerberos.requests_kerberos is not None def get_options(self): options = super(MappedKerberos, self).get_options() options.extend([ loading.Opt('mutual-auth', required=False, default='optional', help='Configures Kerberos Mutual Authentication'), ]) return options def load_from_options(self, **kwargs): if kwargs.get('mutual_auth'): value = kwargs.get('mutual_auth') if not (value.lower() in ['required', 'optional', 'disabled']): m = ('You need to provide a valid value for kerberos mutual ' 'authentication. It can be one of the following: ' '(required, optional, disabled)') raise exceptions.OptionError(m) return super(MappedKerberos, self).load_from_options(**kwargs) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2847943 keystoneauth1-4.4.0/keystoneauth1/extras/oauth1/0000775000175000017500000000000000000000000021661 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/extras/oauth1/__init__.py0000664000175000017500000000125700000000000023777 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.extras.oauth1 import v3 __all__ = ('V3OAuth1Method', 'V3OAuth1') V3OAuth1Method = v3.OAuth1Method V3OAuth1 = v3.OAuth1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/extras/oauth1/_loading.py0000664000175000017500000000313200000000000024006 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.extras.oauth1 import v3 from keystoneauth1 import loading # NOTE(jamielennox): This is not a BaseV3Loader because we don't want to # include the scoping options like project-id in the option list class V3OAuth1(loading.BaseIdentityLoader): @property def plugin_class(self): return v3.OAuth1 @property def available(self): return v3.oauth1 is not None def get_options(self): options = super(V3OAuth1, self).get_options() options.extend([ loading.Opt('consumer-key', required=True, help='OAuth Consumer ID/Key'), loading.Opt('consumer-secret', required=True, help='OAuth Consumer Secret'), loading.Opt('access-key', required=True, help='OAuth Access Key'), loading.Opt('access-secret', required=True, help='OAuth Access Secret'), ]) return options ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/extras/oauth1/v3.py0000664000175000017500000000533100000000000022565 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Oauth authentication plugins. .. warning:: This module requires installation of an extra package (`oauthlib`) not installed by default. Without the extra package an import error will occur. The extra package can be installed using:: $ pip install keystoneauth['oauth1'] """ import logging try: from oauthlib import oauth1 except ImportError: oauth1 = None from keystoneauth1.identity import v3 __all__ = ('OAuth1Method', 'OAuth1') LOG = logging.getLogger(__name__) class OAuth1Method(v3.AuthMethod): """OAuth based authentication method. :param string consumer_key: Consumer key. :param string consumer_secret: Consumer secret. :param string access_key: Access token key. :param string access_secret: Access token secret. """ _method_parameters = ['consumer_key', 'consumer_secret', 'access_key', 'access_secret'] def get_auth_data(self, session, auth, headers, **kwargs): # Add the oauth specific content into the headers oauth_client = oauth1.Client(self.consumer_key, client_secret=self.consumer_secret, resource_owner_key=self.access_key, resource_owner_secret=self.access_secret, signature_method=oauth1.SIGNATURE_HMAC) o_url, o_headers, o_body = oauth_client.sign(auth.token_url, http_method='POST') headers.update(o_headers) return 'oauth1', {} def get_cache_id_elements(self): return dict(('oauth1_%s' % p, getattr(self, p)) for p in self._method_parameters) class OAuth1(v3.AuthConstructor): _auth_method_class = OAuth1Method def __init__(self, *args, **kwargs): super(OAuth1, self).__init__(*args, **kwargs) if self.has_scope_parameters: LOG.warning('Scoping parameters such as a project were provided ' 'to the OAuth1 plugin. Because OAuth1 access is ' 'always scoped to a project these will be ignored by ' 'the identity server') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2887943 keystoneauth1-4.4.0/keystoneauth1/fixture/0000775000175000017500000000000000000000000020640 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/fixture/__init__.py0000664000175000017500000000306500000000000022755 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Produce keystone compliant structures for use in testing. They are part of the public API because they may be relied upon to generate test tokens for other clients. However they should never be imported into the main client (keystoneauth or other). Because of this there may be dependencies from this module on libraries that are only available in testing. """ # flake8: noqa: F405 from keystoneauth1.fixture.discovery import * # noqa from keystoneauth1.fixture import exception from keystoneauth1.fixture.plugin import * # noqa from keystoneauth1.fixture import v2 from keystoneauth1.fixture import v3 FixtureValidationError = exception.FixtureValidationError V2Token = v2.Token V3Token = v3.Token V3FederationToken = v3.V3FederationToken __all__ = ('DiscoveryList', 'FixtureValidationError', 'LoadingFixture', 'TestPlugin', 'V2Discovery', 'V3Discovery', 'V2Token', 'V3Token', 'V3FederationToken', 'VersionDiscovery', ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/fixture/discovery.py0000664000175000017500000002655100000000000023232 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import _utils as utils __all__ = ('DiscoveryList', 'V2Discovery', 'V3Discovery', 'VersionDiscovery', ) _DEFAULT_DAYS_AGO = 30 class DiscoveryBase(dict): """The basic version discovery structure. All version discovery elements should have access to these values. :param string id: The version id for this version entry. :param string status: The status of this entry. :param DateTime updated: When the API was last updated. """ def __init__(self, id, status=None, updated=None): super(DiscoveryBase, self).__init__() self.id = id self.status = status or 'stable' self.updated = updated or utils.before_utcnow(days=_DEFAULT_DAYS_AGO) @property def id(self): return self.get('id') @id.setter def id(self, value): self['id'] = value @property def status(self): return self.get('status') @status.setter def status(self, value): self['status'] = value @property def links(self): return self.setdefault('links', []) @property def updated_str(self): return self.get('updated') @updated_str.setter def updated_str(self, value): self['updated'] = value @property def updated(self): return utils.parse_isotime(self.updated_str) @updated.setter def updated(self, value): self.updated_str = value.isoformat() def add_link(self, href, rel='self', type=None): link = {'href': href, 'rel': rel} if type: link['type'] = type self.links.append(link) return link @property def media_types(self): return self.setdefault('media-types', []) def add_media_type(self, base, type): mt = {'base': base, 'type': type} self.media_types.append(mt) return mt class VersionDiscovery(DiscoveryBase): """A Version element for non-keystone services without microversions. Provides some default values and helper methods for creating a microversion endpoint version structure. Clients should use this instead of creating their own structures. :param string href: The url that this entry should point to. :param string id: The version id that should be reported. """ def __init__(self, href, id, **kwargs): super(VersionDiscovery, self).__init__(id, **kwargs) self.add_link(href) class MicroversionDiscovery(DiscoveryBase): """A Version element that has microversions. Provides some default values and helper methods for creating a microversion endpoint version structure. Clients should use this instead of creating their own structures. :param string href: The url that this entry should point to. :param string id: The version id that should be reported. :param string min_version: The minimum supported microversion. (optional) :param string max_version: The maximum supported microversion. (optional) """ def __init__(self, href, id, min_version='', max_version='', **kwargs): super(MicroversionDiscovery, self).__init__(id, **kwargs) self.add_link(href) self.min_version = min_version self.max_version = max_version @property def min_version(self): return self.get('min_version') @min_version.setter def min_version(self, value): self['min_version'] = value @property def max_version(self): return self.get('max_version') @max_version.setter def max_version(self, value): self['max_version'] = value class NovaMicroversionDiscovery(DiscoveryBase): """A Version element with nova-style microversions. Provides some default values and helper methods for creating a microversion endpoint version structure. Clients should use this instead of creating their own structures. :param href: The url that this entry should point to. :param string id: The version id that should be reported. :param string min_version: The minimum microversion supported. (optional) :param string version: The maximum microversion supported. (optional) """ def __init__(self, href, id, min_version=None, version=None, **kwargs): super(NovaMicroversionDiscovery, self).__init__(id, **kwargs) self.add_link(href) self.min_version = min_version self.version = version @property def min_version(self): return self.get('min_version') @min_version.setter def min_version(self, value): if value: self['min_version'] = value @property def version(self): return self.get('version') @version.setter def version(self, value): if value: self['version'] = value class V2Discovery(DiscoveryBase): """A Version element for a V2 identity service endpoint. Provides some default values and helper methods for creating a v2.0 endpoint version structure. Clients should use this instead of creating their own structures. :param string href: The url that this entry should point to. :param string id: The version id that should be reported. (optional) Defaults to 'v2.0'. :param bool html: Add HTML describedby links to the structure. :param bool pdf: Add PDF describedby links to the structure. """ _DESC_URL = 'https://developer.openstack.org/api-ref/identity/v2/' def __init__(self, href, id=None, html=True, pdf=True, **kwargs): super(V2Discovery, self).__init__(id or 'v2.0', **kwargs) self.add_link(href) if html: self.add_html_description() if pdf: self.add_pdf_description() def add_html_description(self): """Add the HTML described by links. The standard structure includes a link to a HTML document with the API specification. Add it to this entry. """ self.add_link(href=self._DESC_URL + 'content', rel='describedby', type='text/html') def add_pdf_description(self): """Add the PDF described by links. The standard structure includes a link to a PDF document with the API specification. Add it to this entry. """ self.add_link(href=self._DESC_URL + 'identity-dev-guide-2.0.pdf', rel='describedby', type='application/pdf') class V3Discovery(DiscoveryBase): """A Version element for a V3 identity service endpoint. Provides some default values and helper methods for creating a v3 endpoint version structure. Clients should use this instead of creating their own structures. :param href: The url that this entry should point to. :param string id: The version id that should be reported. (optional) Defaults to 'v3.0'. :param bool json: Add JSON media-type elements to the structure. :param bool xml: Add XML media-type elements to the structure. """ def __init__(self, href, id=None, json=True, xml=True, **kwargs): super(V3Discovery, self).__init__(id or 'v3.0', **kwargs) self.add_link(href) if json: self.add_json_media_type() if xml: self.add_xml_media_type() def add_json_media_type(self): """Add the JSON media-type links. The standard structure includes a list of media-types that the endpoint supports. Add JSON to the list. """ self.add_media_type(base='application/json', type='application/vnd.openstack.identity-v3+json') def add_xml_media_type(self): """Add the XML media-type links. The standard structure includes a list of media-types that the endpoint supports. Add XML to the list. """ self.add_media_type(base='application/xml', type='application/vnd.openstack.identity-v3+xml') class DiscoveryList(dict): """A List of version elements. Creates a correctly structured list of identity service endpoints for use in testing with discovery. :param string href: The url that this should be based at. :param bool v2: Add a v2 element. :param bool v3: Add a v3 element. :param string v2_status: The status to use for the v2 element. :param DateTime v2_updated: The update time to use for the v2 element. :param bool v2_html: True to add a html link to the v2 element. :param bool v2_pdf: True to add a pdf link to the v2 element. :param string v3_status: The status to use for the v3 element. :param DateTime v3_updated: The update time to use for the v3 element. :param bool v3_json: True to add a html link to the v2 element. :param bool v3_xml: True to add a pdf link to the v2 element. """ TEST_URL = 'http://keystone.host:5000/' def __init__(self, href=None, v2=True, v3=True, v2_id=None, v3_id=None, v2_status=None, v2_updated=None, v2_html=True, v2_pdf=True, v3_status=None, v3_updated=None, v3_json=True, v3_xml=True): super(DiscoveryList, self).__init__(versions={'values': []}) href = href or self.TEST_URL if v2: v2_href = href.rstrip('/') + '/v2.0' self.add_v2(v2_href, id=v2_id, status=v2_status, updated=v2_updated, html=v2_html, pdf=v2_pdf) if v3: v3_href = href.rstrip('/') + '/v3' self.add_v3(v3_href, id=v3_id, status=v3_status, updated=v3_updated, json=v3_json, xml=v3_xml) @property def versions(self): return self['versions']['values'] def add_version(self, version): """Add a new version structure to the list. :param dict version: A new version structure to add to the list. """ self.versions.append(version) def add_v2(self, href, **kwargs): """Add a v2 version to the list. The parameters are the same as V2Discovery. """ obj = V2Discovery(href, **kwargs) self.add_version(obj) return obj def add_v3(self, href, **kwargs): """Add a v3 version to the list. The parameters are the same as V3Discovery. """ obj = V3Discovery(href, **kwargs) self.add_version(obj) return obj def add_microversion(self, href, id, **kwargs): """Add a microversion version to the list. The parameters are the same as MicroversionDiscovery. """ obj = MicroversionDiscovery(href=href, id=id, **kwargs) self.add_version(obj) return obj def add_nova_microversion(self, href, id, **kwargs): """Add a nova microversion version to the list. The parameters are the same as NovaMicroversionDiscovery. """ obj = NovaMicroversionDiscovery(href=href, id=id, **kwargs) self.add_version(obj) return obj ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/fixture/exception.py0000664000175000017500000000146500000000000023216 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. class FixtureValidationError(Exception): """The token you created is not legitimate. The data contained in the token that was generated is not valid and would not have been returned from a keystone server. You should not do testing with this token. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/fixture/hooks.py0000664000175000017500000000442000000000000022335 0ustar00zuulzuul00000000000000# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Custom hooks for betamax and keystoneauth. Module providing a set of hooks specially designed for interacting with clouds and keystone authentication. :author: Yolanda Robla """ import json def mask_fixture_values(nested, prev_key): for key, value in nested.items(): if isinstance(value, dict): mask_fixture_values(value, key) else: if key in ('tenantName', 'username'): nested[key] = 'dummy' elif prev_key in ('user', 'project', 'tenant') and key == 'name': nested[key] = 'dummy' elif prev_key == 'domain' and key == 'id': nested[key] = 'dummy' elif key == 'password': nested[key] = '********' elif prev_key == 'token' and key in ('expires', 'expires_at'): nested[key] = '9999-12-31T23:59:59Z' def pre_record_hook(interaction, cassette): """Hook to mask saved data. This hook will be triggered before saving the interaction, and will perform two tasks: - mask user, project and password in the saved data - set token expiration time to an inifinite time. """ request_body = interaction.data['request']['body'] if request_body.get('string'): parsed_content = json.loads(request_body['string']) mask_fixture_values(parsed_content, None) request_body['string'] = json.dumps(parsed_content) response_body = interaction.data['response']['body'] if response_body.get('string'): parsed_content = json.loads(response_body['string']) mask_fixture_values(parsed_content, None) response_body['string'] = json.dumps(parsed_content) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/fixture/keystoneauth_betamax.py0000664000175000017500000001267000000000000025444 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """A fixture to wrap the session constructor for use with Betamax.""" from functools import partial from unittest import mock import betamax import fixtures import requests from keystoneauth1.fixture import hooks from keystoneauth1.fixture import serializer as yaml_serializer from keystoneauth1 import session class BetamaxFixture(fixtures.Fixture): def __init__(self, cassette_name, cassette_library_dir=None, serializer=None, record=False, pre_record_hook=hooks.pre_record_hook, serializer_name=None, request_matchers=None): """Configure Betamax for the test suite. :param str cassette_name: This is simply the name of the cassette without any file extension or containing directory. For example, to generate ``keystoneauth1/tests/unit/data/example.yaml``, one would pass only ``example``. :param str cassette_library_dir: This is the directory that will contain all cassette files. In ``keystoneauth1/tests/unit/data/example.yaml`` you would pass ``keystoneauth1/tests/unit/data/``. :param serializer: A class that implements the Serializer API in Betamax. See also: https://betamax.readthedocs.io/en/latest/serializers.html :param record: The Betamax record mode to use. If ``False`` (the default), then Betamax will not record anything. For more information about record modes, see: https://betamax.readthedocs.io/en/latest/record_modes.html :param callable pre_record_hook: Function or callable to use to perform some handling of the request or response data prior to saving it to disk. :param str serializer_name: The name of a serializer already registered with Betamax to use to handle cassettes. For example, if you want to use the default Betamax serializer, you would pass ``'json'`` to this parameter. :param list request_matchers: The list of request matcher names to use with Betamax. Betamax's default list is used if none are specified. See also: https://betamax.readthedocs.io/en/latest/matchers.html """ self.cassette_library_dir = cassette_library_dir self.record = record self.cassette_name = cassette_name if not (serializer or serializer_name): serializer = yaml_serializer.YamlJsonSerializer serializer_name = serializer.name if serializer: betamax.Betamax.register_serializer(serializer) self.serializer = serializer self._serializer_name = serializer_name self.pre_record_hook = pre_record_hook self.use_cassette_kwargs = {} if request_matchers is not None: self.use_cassette_kwargs['match_requests_on'] = request_matchers @property def serializer_name(self): """Determine the name of the selected serializer. If a class was specified, use the name attribute to generate this, otherwise, use the serializer_name parameter from ``__init__``. :returns: Name of the serializer :rtype: str """ if self.serializer: return self.serializer.name return self._serializer_name def setUp(self): super(BetamaxFixture, self).setUp() self.mockpatch = mock.patch.object( session, '_construct_session', partial(_construct_session_with_betamax, self)) self.mockpatch.start() # Unpatch during cleanup self.addCleanup(self.mockpatch.stop) def _construct_session_with_betamax(fixture, session_obj=None): # NOTE(morganfainberg): This function should contain the logic of # keystoneauth1.session._construct_session as it replaces the # _construct_session function to apply betamax magic to the requests # session object. if not session_obj: session_obj = requests.Session() # Use TCPKeepAliveAdapter to fix bug 1323862 for scheme in list(session_obj.adapters.keys()): session_obj.mount(scheme, session.TCPKeepAliveAdapter()) with betamax.Betamax.configure() as config: config.before_record(callback=fixture.pre_record_hook) fixture.recorder = betamax.Betamax( session_obj, cassette_library_dir=fixture.cassette_library_dir) record = 'none' serializer = None if fixture.record in ['once', 'all', 'new_episodes']: record = fixture.record serializer = fixture.serializer_name fixture.recorder.use_cassette(fixture.cassette_name, serialize_with=serializer, record=record, **fixture.use_cassette_kwargs) fixture.recorder.start() fixture.addCleanup(fixture.recorder.stop) return session_obj ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/fixture/plugin.py0000664000175000017500000001421100000000000022507 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid import fixtures from keystoneauth1 import discover from keystoneauth1 import loading from keystoneauth1 import plugin __all__ = ( 'LoadingFixture', 'TestPlugin', ) DEFAULT_TEST_ENDPOINT = 'https://openstack.example.com/%(service_type)s' def _format_endpoint(endpoint, **kwargs): # can't format AUTH_INTERFACE object so replace with string if kwargs.get('service_type') is plugin.AUTH_INTERFACE: kwargs['service_type'] = 'identity' version = kwargs.get('version') if version: discover.normalize_version_number(version) kwargs['version'] = ".".join(str(v) for v in version) return endpoint % kwargs # pass kwargs ok? class TestPlugin(plugin.BaseAuthPlugin): """A simple plugin that returns what you gave it for testing. When testing services that use authentication plugins you often want to stub out the authentication calls and focus on the important part of your service. This plugin acts like a real keystoneauth plugin and returns known standard values without having to stub out real keystone responses. Note that this plugin is a BaseAuthPlugin and not a BaseIdentityPlugin. This means it implements the basic plugin interface that services should be using but does not implement get_auth_ref. get_auth_ref should not be relied upon by services because a user could always configure the service to use a non-keystone auth. :param str token: The token to include in authenticated requests. :param str endpoint: The endpoint to respond to service lookups with. :param str user_id: The user_id to report for the authenticated user. :param str project_id: The project_id to report for the authenticated user. """ auth_type = 'test_plugin' def __init__(self, token=None, endpoint=None, user_id=None, project_id=None): super(TestPlugin, self).__init__() self.token = token or uuid.uuid4().hex self.endpoint = endpoint or DEFAULT_TEST_ENDPOINT self.user_id = user_id or uuid.uuid4().hex self.project_id = project_id or uuid.uuid4().hex def get_endpoint(self, session, **kwargs): return _format_endpoint(self.endpoint, **kwargs) def get_token(self, session, **kwargs): return self.token def get_user_id(self, session, **kwargs): return self.user_id def get_project_id(self, session, **kwargs): return self.project_id def invalidate(self): self.token = uuid.uuid4().hex return True # NOTE(jamielennox): You'll notice there's no get_access/get_auth_ref # function here. These functions are only part of identity plugins, which # whilst the most common are not the only way you can authenticate. You're # application should really only rely on the presence of the above # functions, everything else is on a best effort basis. class _TestPluginLoader(loading.BaseLoader): def __init__(self, plugin): super(_TestPluginLoader, self).__init__() self._plugin = plugin def create_plugin(self, **kwargs): return self._plugin def get_options(self): return [] class LoadingFixture(fixtures.Fixture): """A fixture that will stub out all plugin loading calls. When using keystoneauth plugins loaded from config, CLI or elsewhere it is often difficult to handle the plugin parts in tests because we don't have a reasonable default. This fixture will create a :py:class:`TestPlugin` that will be returned for all calls to plugin loading so you can simply bypass the authentication steps and return something well known. :param str token: The token to include in authenticated requests. :param str endpoint: The endpoint to respond to service lookups with. :param str user_id: The user_id to report for the authenticated user. :param str project_id: The project_id to report for the authenticated user. """ MOCK_POINT = 'keystoneauth1.loading.base.get_plugin_loader' def __init__(self, token=None, endpoint=None, user_id=None, project_id=None): super(LoadingFixture, self).__init__() # these are created and saved here so that a test could use them self.token = token or uuid.uuid4().hex self.endpoint = endpoint or DEFAULT_TEST_ENDPOINT self.user_id = user_id or uuid.uuid4().hex self.project_id = project_id or uuid.uuid4().hex def setUp(self): super(LoadingFixture, self).setUp() self.useFixture(fixtures.MonkeyPatch(self.MOCK_POINT, self.get_plugin_loader)) def create_plugin(self): return TestPlugin(token=self.token, endpoint=self.endpoint, user_id=self.user_id, project_id=self.project_id) def get_plugin_loader(self, auth_type): plugin = self.create_plugin() plugin.auth_type = auth_type return _TestPluginLoader(plugin) def get_endpoint(self, path=None, **kwargs): """Utility function to get the endpoint the plugin would return. This function is provided as a convenience so you can do comparisons in your tests. Overriding it will not affect the endpoint returned by the plugin. :param str path: The path to append to the plugin endpoint. """ endpoint = _format_endpoint(self.endpoint, **kwargs) if path: endpoint = "%s/%s" % (endpoint.rstrip('/'), path.lstrip('/')) return endpoint ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/fixture/serializer.py0000664000175000017500000000567000000000000023373 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """A serializer to emit YAML but with request body in nicely formatted JSON.""" import json import os import betamax.serializers.base import six import yaml def _should_use_block(value): for c in u"\u000a\u000d\u001c\u001d\u001e\u0085\u2028\u2029": if c in value: return True return False def _represent_scalar(self, tag, value, style=None): if style is None: if _should_use_block(value): style = '|' else: style = self.default_style node = yaml.representer.ScalarNode(tag, value, style=style) if self.alias_key is not None: self.represented_objects[self.alias_key] = node return node def _unicode_representer(dumper, uni): node = yaml.ScalarNode(tag=u'tag:yaml.org,2002:str', value=uni) return node def _indent_json(val): if not val: return '' return json.dumps( json.loads(val), indent=2, separators=(',', ': '), sort_keys=False, default=six.text_type) def _is_json_body(interaction): content_type = interaction['headers'].get('Content-Type', []) return 'application/json' in content_type class YamlJsonSerializer(betamax.serializers.base.BaseSerializer): name = "yamljson" @staticmethod def generate_cassette_name(cassette_library_dir, cassette_name): return os.path.join( cassette_library_dir, "{name}.yaml".format(name=cassette_name)) def serialize(self, cassette_data): # Reserialize internal json with indentation for interaction in cassette_data['http_interactions']: for key in ('request', 'response'): if _is_json_body(interaction[key]): interaction[key]['body']['string'] = _indent_json( interaction[key]['body']['string']) class MyDumper(yaml.Dumper): """Specialized Dumper which does nice blocks and unicode.""" yaml.representer.BaseRepresenter.represent_scalar = _represent_scalar MyDumper.add_representer(six.text_type, _unicode_representer) return yaml.dump( cassette_data, Dumper=MyDumper, default_flow_style=False) def deserialize(self, cassette_data): try: deserialized = yaml.safe_load(cassette_data) except yaml.error.YAMLError: deserialized = None if deserialized is not None: return deserialized return {} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/fixture/v2.py0000664000175000017500000001703100000000000021543 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime import uuid from keystoneauth1 import _utils from keystoneauth1.fixture import exception class _Service(dict): def add_endpoint(self, public, admin=None, internal=None, tenant_id=None, region=None, id=None): data = {'tenantId': tenant_id or uuid.uuid4().hex, 'publicURL': public, 'adminURL': admin or public, 'internalURL': internal or public, 'region': region, 'id': id or uuid.uuid4().hex} self.setdefault('endpoints', []).append(data) return data class Token(dict): """A V2 Keystone token that can be used for testing. This object is designed to allow clients to generate a correct V2 token for use in there test code. It should prevent clients from having to know the correct token format and allow them to test the portions of token handling that matter to them and not copy and paste sample. """ def __init__(self, token_id=None, expires=None, issued=None, tenant_id=None, tenant_name=None, user_id=None, user_name=None, trust_id=None, trustee_user_id=None, audit_id=None, audit_chain_id=None): super(Token, self).__init__() self.token_id = token_id or uuid.uuid4().hex self.user_id = user_id or uuid.uuid4().hex self.user_name = user_name or uuid.uuid4().hex self.audit_id = audit_id or uuid.uuid4().hex if not issued: issued = _utils.before_utcnow(minutes=2) if not expires: expires = issued + datetime.timedelta(hours=1) try: self.issued = issued except (TypeError, AttributeError): # issued should be able to be passed as a string so ignore self.issued_str = issued try: self.expires = expires except (TypeError, AttributeError): # expires should be able to be passed as a string so ignore self.expires_str = expires if tenant_id or tenant_name: self.set_scope(tenant_id, tenant_name) if trust_id or trustee_user_id: # the trustee_user_id will generally be the same as the user_id as # the token is being issued to the trustee self.set_trust(id=trust_id, trustee_user_id=trustee_user_id or user_id) if audit_chain_id: self.audit_chain_id = audit_chain_id @property def root(self): return self.setdefault('access', {}) @property def _token(self): return self.root.setdefault('token', {}) @property def token_id(self): return self._token['id'] @token_id.setter def token_id(self, value): self._token['id'] = value @property def expires_str(self): return self._token['expires'] @expires_str.setter def expires_str(self, value): self._token['expires'] = value @property def expires(self): return _utils.parse_isotime(self.expires_str) @expires.setter def expires(self, value): self.expires_str = value.isoformat() @property def issued_str(self): return self._token['issued_at'] @issued_str.setter def issued_str(self, value): self._token['issued_at'] = value @property def issued(self): return _utils.parse_isotime(self.issued_str) @issued.setter def issued(self, value): self.issued_str = value.isoformat() @property def _user(self): return self.root.setdefault('user', {}) @property def user_id(self): return self._user['id'] @user_id.setter def user_id(self, value): self._user['id'] = value @property def user_name(self): return self._user['name'] @user_name.setter def user_name(self, value): self._user['name'] = value @property def tenant_id(self): return self._token.get('tenant', {}).get('id') @tenant_id.setter def tenant_id(self, value): self._token.setdefault('tenant', {})['id'] = value @property def tenant_name(self): return self._token.get('tenant', {}).get('name') @tenant_name.setter def tenant_name(self, value): self._token.setdefault('tenant', {})['name'] = value @property def _metadata(self): return self.root.setdefault('metadata', {}) @property def trust_id(self): return self.root.setdefault('trust', {}).get('id') @trust_id.setter def trust_id(self, value): self.root.setdefault('trust', {})['id'] = value @property def trustee_user_id(self): return self.root.setdefault('trust', {}).get('trustee_user_id') @trustee_user_id.setter def trustee_user_id(self, value): self.root.setdefault('trust', {})['trustee_user_id'] = value @property def audit_id(self): try: return self._token.get('audit_ids', [])[0] except IndexError: return None @audit_id.setter def audit_id(self, value): audit_chain_id = self.audit_chain_id lval = [value] if audit_chain_id else [value, audit_chain_id] self._token['audit_ids'] = lval @property def audit_chain_id(self): try: return self._token.get('audit_ids', [])[1] except IndexError: return None @audit_chain_id.setter def audit_chain_id(self, value): self._token['audit_ids'] = [self.audit_id, value] def validate(self): scoped = 'tenant' in self.token catalog = self.root.get('serviceCatalog') if catalog and not scoped: msg = 'You cannot have a service catalog on an unscoped token' raise exception.FixtureValidationError(msg) if scoped and not self.user.get('roles'): msg = 'You must have roles on a token to scope it' raise exception.FixtureValidationError(msg) def add_role(self, name=None, id=None): id = id or uuid.uuid4().hex name = name or uuid.uuid4().hex roles = self._user.setdefault('roles', []) roles.append({'name': name}) self._metadata.setdefault('roles', []).append(id) return {'id': id, 'name': name} def add_service(self, type, name=None): name = name or uuid.uuid4().hex service = _Service(name=name, type=type) self.root.setdefault('serviceCatalog', []).append(service) return service def remove_service(self, type): self.root['serviceCatalog'] = [ f for f in self.root.setdefault('serviceCatalog', []) if f['type'] != type] def set_scope(self, id=None, name=None): self.tenant_id = id or uuid.uuid4().hex self.tenant_name = name or uuid.uuid4().hex def set_trust(self, id=None, trustee_user_id=None): self.trust_id = id or uuid.uuid4().hex self.trustee_user_id = trustee_user_id or uuid.uuid4().hex def set_bind(self, name, data): self._token.setdefault('bind', {})[name] = data ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/fixture/v3.py0000664000175000017500000004227000000000000021547 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime import uuid from keystoneauth1 import _utils from keystoneauth1.fixture import exception class _Service(dict): """One of the services that exist in the catalog. You use this by adding a service to a token which returns an instance of this object and then you can add_endpoints to the service. """ def add_endpoint(self, interface, url, region=None, id=None): data = {'id': id or uuid.uuid4().hex, 'interface': interface, 'url': url, 'region': region, 'region_id': region} self.setdefault('endpoints', []).append(data) return data def add_standard_endpoints(self, public=None, admin=None, internal=None, region=None): ret = [] if public: ret.append(self.add_endpoint('public', public, region=region)) if admin: ret.append(self.add_endpoint('admin', admin, region=region)) if internal: ret.append(self.add_endpoint('internal', internal, region=region)) return ret class Token(dict): """A V3 Keystone token that can be used for testing. This object is designed to allow clients to generate a correct V3 token for use in there test code. It should prevent clients from having to know the correct token format and allow them to test the portions of token handling that matter to them and not copy and paste sample. """ def __init__(self, expires=None, issued=None, user_id=None, user_name=None, user_domain_id=None, user_domain_name=None, methods=None, project_id=None, project_name=None, project_domain_id=None, project_domain_name=None, domain_id=None, domain_name=None, trust_id=None, trust_impersonation=None, trustee_user_id=None, trustor_user_id=None, application_credential_id=None, application_credential_access_rules=None, oauth_access_token_id=None, oauth_consumer_id=None, audit_id=None, audit_chain_id=None, is_admin_project=None, project_is_domain=None): super(Token, self).__init__() self.user_id = user_id or uuid.uuid4().hex self.user_name = user_name or uuid.uuid4().hex self.user_domain_id = user_domain_id or uuid.uuid4().hex self.user_domain_name = user_domain_name or uuid.uuid4().hex self.audit_id = audit_id or uuid.uuid4().hex if not methods: methods = ['password'] self.methods.extend(methods) if not issued: issued = _utils.before_utcnow(minutes=2) try: self.issued = issued except (TypeError, AttributeError): # issued should be able to be passed as a string so ignore self.issued_str = issued if not expires: expires = self.issued + datetime.timedelta(hours=1) try: self.expires = expires except (TypeError, AttributeError): # expires should be able to be passed as a string so ignore self.expires_str = expires if (project_id or project_name or project_domain_id or project_domain_name): self.set_project_scope(id=project_id, name=project_name, domain_id=project_domain_id, domain_name=project_domain_name, is_domain=project_is_domain) if domain_id or domain_name: self.set_domain_scope(id=domain_id, name=domain_name) if (trust_id or (trust_impersonation is not None) or trustee_user_id or trustor_user_id): self.set_trust_scope(id=trust_id, impersonation=trust_impersonation, trustee_user_id=trustee_user_id, trustor_user_id=trustor_user_id) if application_credential_id: self.set_application_credential( application_credential_id, access_rules=application_credential_access_rules) if oauth_access_token_id or oauth_consumer_id: self.set_oauth(access_token_id=oauth_access_token_id, consumer_id=oauth_consumer_id) if audit_chain_id: self.audit_chain_id = audit_chain_id if is_admin_project is not None: self.is_admin_project = is_admin_project @property def root(self): return self.setdefault('token', {}) @property def expires_str(self): return self.root.get('expires_at') @expires_str.setter def expires_str(self, value): self.root['expires_at'] = value @property def expires(self): return _utils.parse_isotime(self.expires_str) @expires.setter def expires(self, value): self.expires_str = value.isoformat() @property def issued_str(self): return self.root.get('issued_at') @issued_str.setter def issued_str(self, value): self.root['issued_at'] = value @property def issued(self): return _utils.parse_isotime(self.issued_str) @issued.setter def issued(self, value): self.issued_str = value.isoformat() @property def _user(self): return self.root.setdefault('user', {}) @property def user_id(self): return self._user.get('id') @user_id.setter def user_id(self, value): self._user['id'] = value @property def user_name(self): return self._user.get('name') @user_name.setter def user_name(self, value): self._user['name'] = value @property def _user_domain(self): return self._user.setdefault('domain', {}) @_user_domain.setter def _user_domain(self, domain): self._user['domain'] = domain @property def user_domain_id(self): return self._user_domain.get('id') @user_domain_id.setter def user_domain_id(self, value): self._user_domain['id'] = value @property def user_domain_name(self): return self._user_domain.get('name') @user_domain_name.setter def user_domain_name(self, value): self._user_domain['name'] = value @property def methods(self): return self.root.setdefault('methods', []) @property def project_id(self): return self.root.get('project', {}).get('id') @project_id.setter def project_id(self, value): self.root.setdefault('project', {})['id'] = value @property def project_is_domain(self): return self.root.get('is_domain') @project_is_domain.setter def project_is_domain(self, value): self.root['is_domain'] = value @property def project_name(self): return self.root.get('project', {}).get('name') @project_name.setter def project_name(self, value): self.root.setdefault('project', {})['name'] = value @property def project_domain_id(self): return self.root.get('project', {}).get('domain', {}).get('id') @project_domain_id.setter def project_domain_id(self, value): project = self.root.setdefault('project', {}) project.setdefault('domain', {})['id'] = value @property def project_domain_name(self): return self.root.get('project', {}).get('domain', {}).get('name') @project_domain_name.setter def project_domain_name(self, value): project = self.root.setdefault('project', {}) project.setdefault('domain', {})['name'] = value @property def domain_id(self): return self.root.get('domain', {}).get('id') @domain_id.setter def domain_id(self, value): self.root.setdefault('domain', {})['id'] = value @property def domain_name(self): return self.root.get('domain', {}).get('name') @domain_name.setter def domain_name(self, value): self.root.setdefault('domain', {})['name'] = value @property def system(self): return self.root.get('system', {}) @system.setter def system(self, value): return self.root.setdefault('system', value) @property def trust_id(self): return self.root.get('OS-TRUST:trust', {}).get('id') @trust_id.setter def trust_id(self, value): self.root.setdefault('OS-TRUST:trust', {})['id'] = value @property def trust_impersonation(self): return self.root.get('OS-TRUST:trust', {}).get('impersonation') @trust_impersonation.setter def trust_impersonation(self, value): self.root.setdefault('OS-TRUST:trust', {})['impersonation'] = value @property def trustee_user_id(self): trust = self.root.get('OS-TRUST:trust', {}) return trust.get('trustee_user', {}).get('id') @trustee_user_id.setter def trustee_user_id(self, value): trust = self.root.setdefault('OS-TRUST:trust', {}) trust.setdefault('trustee_user', {})['id'] = value @property def trustor_user_id(self): trust = self.root.get('OS-TRUST:trust', {}) return trust.get('trustor_user', {}).get('id') @trustor_user_id.setter def trustor_user_id(self, value): trust = self.root.setdefault('OS-TRUST:trust', {}) trust.setdefault('trustor_user', {})['id'] = value @property def application_credential_id(self): return self.root.get('application_credential', {}).get('id') @application_credential_id.setter def application_credential_id(self, value): application_credential = self.root.setdefault( 'application_credential', {}) application_credential.setdefault('id', value) @property def application_credential_access_rules(self): return self.root.get('application_credential', {}).get('access_rules') @application_credential_access_rules.setter def application_credential_access_rules(self, value): application_credential = self.root.setdefault( 'application_credential', {}) application_credential.setdefault('access_rules', value) @property def oauth_access_token_id(self): return self.root.get('OS-OAUTH1', {}).get('access_token_id') @oauth_access_token_id.setter def oauth_access_token_id(self, value): self.root.setdefault('OS-OAUTH1', {})['access_token_id'] = value @property def oauth_consumer_id(self): return self.root.get('OS-OAUTH1', {}).get('consumer_id') @oauth_consumer_id.setter def oauth_consumer_id(self, value): self.root.setdefault('OS-OAUTH1', {})['consumer_id'] = value @property def audit_id(self): try: return self.root.get('audit_ids', [])[0] except IndexError: return None @audit_id.setter def audit_id(self, value): audit_chain_id = self.audit_chain_id lval = [value] if audit_chain_id else [value, audit_chain_id] self.root['audit_ids'] = lval @property def audit_chain_id(self): try: return self.root.get('audit_ids', [])[1] except IndexError: return None @audit_chain_id.setter def audit_chain_id(self, value): self.root['audit_ids'] = [self.audit_id, value] @property def role_ids(self): return [r['id'] for r in self.root.get('roles', [])] @property def role_names(self): return [r['name'] for r in self.root.get('roles', [])] @property def is_admin_project(self): return self.root.get('is_admin_project') @is_admin_project.setter def is_admin_project(self, value): self.root['is_admin_project'] = value @is_admin_project.deleter def is_admin_project(self): self.root.pop('is_admin_project', None) def validate(self): project = self.root.get('project') domain = self.root.get('domain') system = self.root.get('system') trust = self.root.get('OS-TRUST:trust') catalog = self.root.get('catalog') roles = self.root.get('roles') scoped = project or domain or trust if sum((bool(project), bool(domain), bool(trust), bool(system))) > 1: msg = 'You cannot scope to multiple targets' raise exception.FixtureValidationError(msg) if catalog and not scoped: msg = 'You cannot have a service catalog on an unscoped token' raise exception.FixtureValidationError(msg) if scoped and not self.user.get('roles'): msg = 'You must have roles on a token to scope it' raise exception.FixtureValidationError(msg) if bool(scoped) != bool(roles): msg = 'You must be scoped to have roles and vice-versa' raise exception.FixtureValidationError(msg) def add_role(self, name=None, id=None): roles = self.root.setdefault('roles', []) data = {'id': id or uuid.uuid4().hex, 'name': name or uuid.uuid4().hex} roles.append(data) return data def add_service(self, type, name=None, id=None): service = _Service(type=type, id=id or uuid.uuid4().hex) if name: service['name'] = name self.root.setdefault('catalog', []).append(service) return service def remove_service(self, type): self.root.setdefault('catalog', []) self.root['catalog'] = [ f for f in self.root.setdefault('catalog', []) if f['type'] != type] def set_project_scope(self, id=None, name=None, domain_id=None, domain_name=None, is_domain=None): self.project_id = id or uuid.uuid4().hex self.project_name = name or uuid.uuid4().hex self.project_domain_id = domain_id or uuid.uuid4().hex self.project_domain_name = domain_name or uuid.uuid4().hex if is_domain is not None: self.project_is_domain = is_domain def set_domain_scope(self, id=None, name=None): self.domain_id = id or uuid.uuid4().hex self.domain_name = name or uuid.uuid4().hex def set_system_scope(self): # NOTE(lbragstad): In the future it might be possible to scope a token # to a subset of the entire system (e.g. a specific service, region, or # service within a region). Until then, the only system scope is the # entire system. self.system = {'all': True} def set_trust_scope(self, id=None, impersonation=False, trustee_user_id=None, trustor_user_id=None): self.trust_id = id or uuid.uuid4().hex self.trust_impersonation = impersonation self.trustee_user_id = trustee_user_id or uuid.uuid4().hex self.trustor_user_id = trustor_user_id or uuid.uuid4().hex def set_oauth(self, access_token_id=None, consumer_id=None): self.oauth_access_token_id = access_token_id or uuid.uuid4().hex self.oauth_consumer_id = consumer_id or uuid.uuid4().hex def set_application_credential(self, application_credential_id, access_rules=None): self.application_credential_id = application_credential_id if access_rules is not None: self.application_credential_access_rules = access_rules @property def service_providers(self): return self.root.get('service_providers') def add_service_provider(self, sp_id, sp_auth_url, sp_url): _service_providers = self.root.setdefault('service_providers', []) sp = {'id': sp_id, 'auth_url': sp_auth_url, 'sp_url': sp_url} _service_providers.append(sp) return sp def set_bind(self, name, data): self.root.setdefault('bind', {})[name] = data class V3FederationToken(Token): """A V3 Keystone Federation token that can be used for testing. Similar to V3Token, this object is designed to allow clients to generate a correct V3 federation token for use in test code. """ FEDERATED_DOMAIN_ID = 'Federated' def __init__(self, methods=None, identity_provider=None, protocol=None, groups=None): methods = methods or ['saml2'] super(V3FederationToken, self).__init__(methods=methods) self._user_domain = {'id': V3FederationToken.FEDERATED_DOMAIN_ID} self.add_federation_info_to_user(identity_provider, protocol, groups) def add_federation_info_to_user(self, identity_provider=None, protocol=None, groups=None): data = { "OS-FEDERATION": { "identity_provider": identity_provider or uuid.uuid4().hex, "protocol": protocol or uuid.uuid4().hex, "groups": groups or [{"id": uuid.uuid4().hex}] } } self._user.update(data) return data ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2887943 keystoneauth1-4.4.0/keystoneauth1/hacking/0000775000175000017500000000000000000000000020556 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/hacking/__init__.py0000664000175000017500000000000000000000000022655 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/hacking/checks.py0000664000175000017500000000231500000000000022371 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """keystoneauth1's pep8 extensions. In order to make the review process faster and easier for core devs we are adding some keystoneauth1 specific pep8 checks. This will catch common errors so that core devs don't have to. """ import re from hacking import core @core.flake8ext def check_oslo_namespace_imports(logical_line, blank_before, filename): oslo_namespace_imports = re.compile( r"(((from)|(import))\s+oslo\.)|(from\s+oslo\s+import\s+)") if re.match(oslo_namespace_imports, logical_line): msg = ("K333: '%s' must be used instead of '%s'.") % ( logical_line.replace('oslo.', 'oslo_'), logical_line) yield(0, msg) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/http_basic.py0000664000175000017500000000306100000000000021644 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import base64 from keystoneauth1 import plugin AUTH_HEADER_NAME = 'Authorization' class HTTPBasicAuth(plugin.FixedEndpointPlugin): """A provider that will always use HTTP Basic authentication. This is useful to unify session/adapter loading for services that might be deployed in standalone mode. """ def __init__(self, endpoint=None, username=None, password=None): super(HTTPBasicAuth, self).__init__(endpoint) self.username = username self.password = password def get_token(self, session, **kwargs): if self.username is None or self.password is None: return None token = bytes('%s:%s' % (self.username, self.password), encoding='utf-8') encoded = base64.b64encode(token) return str(encoded, encoding='utf-8') def get_headers(self, session, **kwargs): token = self.get_token(session) if not token: return None auth = 'Basic %s' % token return {AUTH_HEADER_NAME: auth} ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2887943 keystoneauth1-4.4.0/keystoneauth1/identity/0000775000175000017500000000000000000000000021003 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/__init__.py0000664000175000017500000000466100000000000023123 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.identity import base from keystoneauth1.identity import generic from keystoneauth1.identity import v2 from keystoneauth1.identity import v3 from keystoneauth1.identity.v3 import oidc BaseIdentityPlugin = base.BaseIdentityPlugin V2Password = v2.Password """See :class:`keystoneauth1.identity.v2.Password`""" V2Token = v2.Token """See :class:`keystoneauth1.identity.v2.Token`""" V3Password = v3.Password """See :class:`keystoneauth1.identity.v3.Password`""" V3Token = v3.Token """See :class:`keystoneauth1.identity.v3.Token`""" Password = generic.Password """See :class:`keystoneauth1.identity.generic.Password`""" Token = generic.Token """See :class:`keystoneauth1.identity.generic.Token`""" V3OidcClientCredentials = oidc.OidcClientCredentials """See :class:`keystoneauth1.identity.v3.oidc.OidcClientCredentials`""" V3OidcPassword = oidc.OidcPassword """See :class:`keystoneauth1.identity.v3.oidc.OidcPassword`""" V3OidcAuthorizationCode = oidc.OidcAuthorizationCode """See :class:`keystoneauth1.identity.v3.oidc.OidcAuthorizationCode`""" V3OidcAccessToken = oidc.OidcAccessToken """See :class:`keystoneauth1.identity.v3.oidc.OidcAccessToken`""" V3TOTP = v3.TOTP """See :class:`keystoneauth1.identity.v3.TOTP`""" V3TokenlessAuth = v3.TokenlessAuth """See :class:`keystoneauth1.identity.v3.TokenlessAuth`""" V3ApplicationCredential = v3.ApplicationCredential """See :class:`keystoneauth1.identity.v3.ApplicationCredential`""" V3MultiFactor = v3.MultiFactor """See :class:`keystoneauth1.identity.v3.MultiFactor`""" __all__ = ('BaseIdentityPlugin', 'Password', 'Token', 'V2Password', 'V2Token', 'V3Password', 'V3Token', 'V3OidcPassword', 'V3OidcAuthorizationCode', 'V3OidcAccessToken', 'V3TOTP', 'V3TokenlessAuth', 'V3ApplicationCredential', 'V3MultiFactor') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/access.py0000664000175000017500000000347400000000000022626 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.identity import base class AccessInfoPlugin(base.BaseIdentityPlugin): """A plugin that turns an existing AccessInfo object into a usable plugin. There are cases where reuse of an auth_ref or AccessInfo object is warranted such as from a cache, from auth_token middleware, or another source. Turn the existing access info object into an identity plugin. This plugin cannot be refreshed as the AccessInfo object does not contain any authorizing information. :param auth_ref: the existing AccessInfo object. :type auth_ref: keystoneauth1.access.AccessInfo :param auth_url: the url where this AccessInfo was retrieved from. Required if using the AUTH_INTERFACE with get_endpoint. (optional) """ def __init__(self, auth_ref, auth_url=None): super(AccessInfoPlugin, self).__init__(auth_url=auth_url, reauthenticate=False) self.auth_ref = auth_ref def get_auth_ref(self, session, **kwargs): return self.auth_ref def invalidate(self): # NOTE(jamielennox): Don't allow the default invalidation to occur # because on next authentication request we will only get the same # auth_ref object again. return False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/base.py0000664000175000017500000007740600000000000022305 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc import base64 import functools import hashlib import json import threading import six from keystoneauth1 import _utils as utils from keystoneauth1 import access from keystoneauth1 import discover from keystoneauth1 import exceptions from keystoneauth1 import plugin LOG = utils.get_logger(__name__) @six.add_metaclass(abc.ABCMeta) class BaseIdentityPlugin(plugin.BaseAuthPlugin): # we count a token as valid (not needing refreshing) if it is valid for at # least this many seconds before the token expiry time MIN_TOKEN_LIFE_SECONDS = 120 def __init__(self, auth_url=None, reauthenticate=True): super(BaseIdentityPlugin, self).__init__() self.auth_url = auth_url self.auth_ref = None self.reauthenticate = reauthenticate self._lock = threading.Lock() @abc.abstractmethod def get_auth_ref(self, session, **kwargs): """Obtain a token from an OpenStack Identity Service. This method is overridden by the various token version plugins. This function should not be called independently and is expected to be invoked via the do_authenticate function. This function will be invoked if the AcessInfo object cached by the plugin is not valid. Thus plugins should always fetch a new AccessInfo when invoked. If you are looking to just retrieve the current auth data then you should use get_access. :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :raises keystoneauth1.exceptions.response.InvalidResponse: The response returned wasn't appropriate. :raises keystoneauth1.exceptions.http.HttpError: An error from an invalid HTTP response. :returns: Token access information. :rtype: :class:`keystoneauth1.access.AccessInfo` """ def get_token(self, session, **kwargs): """Return a valid auth token. If a valid token is not present then a new one will be fetched. :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :raises keystoneauth1.exceptions.http.HttpError: An error from an invalid HTTP response. :return: A valid token. :rtype: string """ return self.get_access(session).auth_token def _needs_reauthenticate(self): """Return if the existing token needs to be re-authenticated. The token should be refreshed if it is about to expire. :returns: True if the plugin should fetch a new token. False otherwise. """ if not self.auth_ref: # authentication was never fetched. return True if not self.reauthenticate: # don't re-authenticate if it has been disallowed. return False if self.auth_ref.will_expire_soon(self.MIN_TOKEN_LIFE_SECONDS): # if it's about to expire we should re-authenticate now. return True # otherwise it's fine and use the existing one. return False def get_access(self, session, **kwargs): """Fetch or return a current AccessInfo object. If a valid AccessInfo is present then it is returned otherwise a new one will be fetched. :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :raises keystoneauth1.exceptions.http.HttpError: An error from an invalid HTTP response. :returns: Valid AccessInfo :rtype: :class:`keystoneauth1.access.AccessInfo` """ # Hey Kids! Thread safety is important particularly in the case where # a service is creating an admin style plugin that will then proceed # to make calls from many threads. As a token expires all the threads # will try and fetch a new token at once, so we want to ensure that # only one thread tries to actually fetch from keystone at once. with self._lock: if self._needs_reauthenticate(): self.auth_ref = self.get_auth_ref(session) return self.auth_ref def invalidate(self): """Invalidate the current authentication data. This should result in fetching a new token on next call. A plugin may be invalidated if an Unauthorized HTTP response is returned to indicate that the token may have been revoked or is otherwise now invalid. :returns: True if there was something that the plugin did to invalidate. This means that it makes sense to try again. If nothing happens returns False to indicate give up. :rtype: bool """ if self.auth_ref: self.auth_ref = None return True return False def get_endpoint_data(self, session, service_type=None, interface=None, region_name=None, service_name=None, allow=None, allow_version_hack=True, discover_versions=True, skip_discovery=False, min_version=None, max_version=None, endpoint_override=None, **kwargs): """Return a valid endpoint data for a service. If a valid token is not present then a new one will be fetched using the session and kwargs. version, min_version and max_version can all be given either as a string or a tuple. Valid interface types: `public` or `publicURL`, `internal` or `internalURL`, `admin` or 'adminURL` :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :param string service_type: The type of service to lookup the endpoint for. This plugin will return None (failure) if service_type is not provided. :param interface: Type of endpoint. Can be a single value or a list of values. If it's a list of values, they will be looked for in order of preference. Can also be `keystoneauth1.plugin.AUTH_INTERFACE` to indicate that the auth_url should be used instead of the value in the catalog. (optional, defaults to public) :param string region_name: The region the endpoint should exist in. (optional) :param string service_name: The name of the service in the catalog. (optional) :param dict allow: Extra filters to pass when discovering API versions. (optional) :param bool allow_version_hack: Allow keystoneauth to hack up catalog URLS to support older schemes. (optional, default True) :param bool discover_versions: Whether to get version metadata from the version discovery document even if it's not neccessary to fulfill the major version request. (optional, defaults to True) :param bool skip_discovery: Whether to skip version discovery even if a version has been given. This is useful if endpoint_override or similar has been given and grabbing additional information about the endpoint is not useful. :param min_version: The minimum version that is acceptable. Mutually exclusive with version. If min_version is given with no max_version it is as if max version is 'latest'. (optional) :param max_version: The maximum version that is acceptable. Mutually exclusive with version. If min_version is given with no max_version it is as if max version is 'latest'. (optional) :param str endpoint_override: URL to use instead of looking in the catalog. Catalog lookup will be skipped, but version discovery will be run. Sets allow_version_hack to False (optional) :param kwargs: Ignored. :raises keystoneauth1.exceptions.http.HttpError: An error from an invalid HTTP response. :return: Valid EndpointData or None if not available. :rtype: `keystoneauth1.discover.EndpointData` or None """ allow = allow or {} min_version, max_version = discover._normalize_version_args( None, min_version, max_version, service_type=service_type) # NOTE(jamielennox): if you specifically ask for requests to be sent to # the auth url then we can ignore many of the checks. Typically if you # are asking for the auth endpoint it means that there is no catalog to # query however we still need to support asking for a specific version # of the auth_url for generic plugins. if interface is plugin.AUTH_INTERFACE: endpoint_data = discover.EndpointData( service_url=self.auth_url, service_type=service_type or 'identity') project_id = None elif endpoint_override: # TODO(mordred) Make a code path that will look for a # matching entry in the catalog if the catalog # exists and fill in the interface, region_name, etc. # For now, just use any information the use has # provided. endpoint_data = discover.EndpointData( service_url=endpoint_override, catalog_url=endpoint_override, interface=interface, region_name=region_name, service_name=service_name) # Setting an endpoint_override then calling get_endpoint_data means # you absolutely want the discovery info for the URL in question. # There are no code flows where this will happen for any other # reasons. allow_version_hack = False project_id = self.get_project_id(session) else: if not service_type: LOG.warning('Plugin cannot return an endpoint without ' 'knowing the service type that is required. Add ' 'service_type to endpoint filtering data.') return None # It's possible for things higher in the stack, because of # defaults, to explicitly pass None. if not interface: interface = 'public' service_catalog = self.get_access(session).service_catalog project_id = self.get_project_id(session) # NOTE(mordred): service_catalog.url_data_for raises if it can't # find a match, so this will always be a valid object. endpoint_data = service_catalog.endpoint_data_for( service_type=service_type, interface=interface, region_name=region_name, service_name=service_name) if not endpoint_data: return None if skip_discovery: return endpoint_data try: return endpoint_data.get_versioned_data( session, project_id=project_id, min_version=min_version, max_version=max_version, cache=self._discovery_cache, discover_versions=discover_versions, allow_version_hack=allow_version_hack, allow=allow) except (exceptions.DiscoveryFailure, exceptions.HttpError, exceptions.ConnectionError): # If a version was requested, we didn't find it, return # None. if max_version or min_version: return None # If one wasn't, then the endpoint_data we already have # should be fine return endpoint_data def get_endpoint(self, session, service_type=None, interface=None, region_name=None, service_name=None, version=None, allow=None, allow_version_hack=True, skip_discovery=False, min_version=None, max_version=None, **kwargs): """Return a valid endpoint for a service. If a valid token is not present then a new one will be fetched using the session and kwargs. version, min_version and max_version can all be given either as a string or a tuple. Valid interface types: `public` or `publicURL`, `internal` or `internalURL`, `admin` or 'adminURL` :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :param string service_type: The type of service to lookup the endpoint for. This plugin will return None (failure) if service_type is not provided. :param interface: Type of endpoint. Can be a single value or a list of values. If it's a list of values, they will be looked for in order of preference. Can also be `keystoneauth1.plugin.AUTH_INTERFACE` to indicate that the auth_url should be used instead of the value in the catalog. (optional, defaults to public) :param string region_name: The region the endpoint should exist in. (optional) :param string service_name: The name of the service in the catalog. (optional) :param version: The minimum version number required for this endpoint. (optional) :param dict allow: Extra filters to pass when discovering API versions. (optional) :param bool allow_version_hack: Allow keystoneauth to hack up catalog URLS to support older schemes. (optional, default True) :param bool skip_discovery: Whether to skip version discovery even if a version has been given. This is useful if endpoint_override or similar has been given and grabbing additional information about the endpoint is not useful. :param min_version: The minimum version that is acceptable. Mutually exclusive with version. If min_version is given with no max_version it is as if max version is 'latest'. (optional) :param max_version: The maximum version that is acceptable. Mutually exclusive with version. If min_version is given with no max_version it is as if max version is 'latest'. (optional) :raises keystoneauth1.exceptions.http.HttpError: An error from an invalid HTTP response. :return: A valid endpoint URL or None if not available. :rtype: string or None """ # Explode `version` into min_version and max_version - everything below # here uses the latter rather than the former. min_version, max_version = discover._normalize_version_args( version, min_version, max_version, service_type=service_type) # Set discover_versions to False since we're only going to return # a URL. Fetching the microversion data would be needlessly # expensive in the common case. However, discover_versions=False # will still run discovery if the version requested is not the # version in the catalog. endpoint_data = self.get_endpoint_data( session, service_type=service_type, interface=interface, region_name=region_name, service_name=service_name, allow=allow, min_version=min_version, max_version=max_version, discover_versions=False, skip_discovery=skip_discovery, allow_version_hack=allow_version_hack, **kwargs) return endpoint_data.url if endpoint_data else None def get_api_major_version(self, session, service_type=None, interface=None, region_name=None, service_name=None, version=None, allow=None, allow_version_hack=True, skip_discovery=False, discover_versions=False, min_version=None, max_version=None, **kwargs): """Return the major API version for a service. If a valid token is not present then a new one will be fetched using the session and kwargs. version, min_version and max_version can all be given either as a string or a tuple. Valid interface types: `public` or `publicURL`, `internal` or `internalURL`, `admin` or 'adminURL` :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :param string service_type: The type of service to lookup the endpoint for. This plugin will return None (failure) if service_type is not provided. :param interface: Type of endpoint. Can be a single value or a list of values. If it's a list of values, they will be looked for in order of preference. Can also be `keystoneauth1.plugin.AUTH_INTERFACE` to indicate that the auth_url should be used instead of the value in the catalog. (optional, defaults to public) :param string region_name: The region the endpoint should exist in. (optional) :param string service_name: The name of the service in the catalog. (optional) :param version: The minimum version number required for this endpoint. (optional) :param dict allow: Extra filters to pass when discovering API versions. (optional) :param bool allow_version_hack: Allow keystoneauth to hack up catalog URLS to support older schemes. (optional, default True) :param bool skip_discovery: Whether to skip version discovery even if a version has been given. This is useful if endpoint_override or similar has been given and grabbing additional information about the endpoint is not useful. :param bool discover_versions: Whether to get version metadata from the version discovery document even if it's not neccessary to fulfill the major version request. Defaults to False because get_endpoint doesn't need metadata. (optional, defaults to False) :param min_version: The minimum version that is acceptable. Mutually exclusive with version. If min_version is given with no max_version it is as if max version is 'latest'. (optional) :param max_version: The maximum version that is acceptable. Mutually exclusive with version. If min_version is given with no max_version it is as if max version is 'latest'. (optional) :raises keystoneauth1.exceptions.http.HttpError: An error from an invalid HTTP response. :return: The major version of the API of the service discovered. :rtype: tuple or None .. note:: Implementation notes follow. Users should not need to wrap their head around these implementation notes. `get_api_major_version` should do what is expected with the least possible cost while still consistently returning a value if possible. There are many cases when major version can be satisfied without actually calling the discovery endpoint (like when the version is in the url). If the user has a cloud with the versioned endpoint ``https://volume.example.com/v3`` in the catalog for the ``block-storage`` service and they do:: client = adapter.Adapter( session, service_type='block-storage', min_version=2, max_version=3) volume_version = client.get_api_major_version() The version actually be returned with no api calls other than getting the token. For that reason, :meth:`.get_api_major_version` first calls :meth:`.get_endpoint_data` with ``discover_versions=False``. If their catalog has an unversioned endpoint ``https://volume.example.com`` for the ``block-storage`` service and they do this:: client = adapter.Adapter(session, service_type='block-storage') client is now set up to "use whatever is in the catalog". Since the url doesn't have a version, :meth:`.get_endpoint_data` with ``discover_versions=False`` will result in ``api_version=None``. (No version was requested so it didn't need to do the round trip) In order to find out what version the endpoint actually is, we must make a round trip. Therefore, if ``api_version`` is ``None`` after the first call, :meth:`.get_api_major_version` will make a second call to :meth:`.get_endpoint_data` with ``discover_versions=True``. """ allow = allow or {} # Explode `version` into min_version and max_version - everything below # here uses the latter rather than the former. min_version, max_version = discover._normalize_version_args( version, min_version, max_version, service_type=service_type) # Using functools.partial here just to reduce copy-pasta of params get_endpoint_data = functools.partial( self.get_endpoint_data, session, service_type=service_type, interface=interface, region_name=region_name, service_name=service_name, allow=allow, min_version=min_version, max_version=max_version, skip_discovery=skip_discovery, allow_version_hack=allow_version_hack, **kwargs) data = get_endpoint_data(discover_versions=discover_versions) if (not data or not data.api_version) and not discover_versions: # It's possible that no version was requested and the endpoint # in the catalog has no version in the URL. A version has been # requested, so now it's ok to run discovery. data = get_endpoint_data(discover_versions=True) if not data: return None return data.api_version def get_all_version_data(self, session, interface='public', region_name=None, service_type=None, **kwargs): """Get version data for all services in the catalog. :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :param interface: Type of endpoint to get version data for. Can be a single value or a list of values. A value of None indicates that all interfaces should be queried. (optional, defaults to public) :param string region_name: Region of endpoints to get version data for. A valueof None indicates that all regions should be queried. (optional, defaults to None) :param string service_type: Limit the version data to a single service. (optional, defaults to None) :returns: A dictionary keyed by region_name with values containing dictionaries keyed by interface with values being a list of :class:`~keystoneauth1.discover.VersionData`. """ service_types = discover._SERVICE_TYPES catalog = self.get_access(session).service_catalog version_data = {} endpoints_data = catalog.get_endpoints_data( interface=interface, region_name=region_name, service_type=service_type, ) for endpoint_service_type, services in endpoints_data.items(): if service_types.is_known(endpoint_service_type): endpoint_service_type = service_types.get_service_type( endpoint_service_type) for service in services: versions = service.get_all_version_string_data( session=session, project_id=self.get_project_id(session), ) if service.region_name not in version_data: version_data[service.region_name] = {} regions = version_data[service.region_name] interface = service.interface.rstrip('URL') if interface not in regions: regions[interface] = {} regions[interface][endpoint_service_type] = versions return version_data def get_user_id(self, session, **kwargs): return self.get_access(session).user_id def get_project_id(self, session, **kwargs): return self.get_access(session).project_id def get_sp_auth_url(self, session, sp_id, **kwargs): try: return self.get_access( session).service_providers.get_auth_url(sp_id) except exceptions.ServiceProviderNotFound: return None def get_sp_url(self, session, sp_id, **kwargs): try: return self.get_access( session).service_providers.get_sp_url(sp_id) except exceptions.ServiceProviderNotFound: return None def get_discovery(self, session, url, authenticated=None): """Return the discovery object for a URL. Check the session and the plugin cache to see if we have already performed discovery on the URL and if so return it, otherwise create a new discovery object, cache it and return it. This function is expected to be used by subclasses and should not be needed by users. :param session: A session object to discover with. :type session: keystoneauth1.session.Session :param str url: The url to lookup. :param bool authenticated: Include a token in the discovery call. (optional) Defaults to None (use a token if a plugin is installed). :raises keystoneauth1.exceptions.discovery.DiscoveryFailure: if for some reason the lookup fails. :raises keystoneauth1.exceptions.http.HttpError: An error from an invalid HTTP response. :returns: A discovery object with the results of looking up that URL. """ return discover.get_discovery(session=session, url=url, cache=self._discovery_cache, authenticated=authenticated) def get_cache_id_elements(self): """Get the elements for this auth plugin that make it unique. As part of the get_cache_id requirement we need to determine what aspects of this plugin and its values that make up the unique elements. This should be overridden by plugins that wish to allow caching. :returns: The unique attributes and values of this plugin. :rtype: A flat dict with a str key and str or None value. This is required as we feed these values into a hash. Pairs where the value is None are ignored in the hashed id. """ raise NotImplementedError() def get_cache_id(self): """Fetch an identifier that uniquely identifies the auth options. The returned identifier need not be decomposable or otherwise provide any way to recreate the plugin. This string MUST change if any of the parameters that are used to uniquely identity this plugin change. It should not change upon a reauthentication of the plugin. :returns: A unique string for the set of options :rtype: str or None if this is unsupported or unavailable. """ try: elements = self.get_cache_id_elements() except NotImplementedError: return None hasher = hashlib.sha256() for k, v in sorted(elements.items()): if v is not None: # NOTE(jamielennox): in python3 you need to pass bytes to hash if isinstance(k, six.string_types): k = k.encode('utf-8') if isinstance(v, six.string_types): v = v.encode('utf-8') hasher.update(k) hasher.update(v) return base64.b64encode(hasher.digest()).decode('utf-8') def get_auth_state(self): """Retrieve the current authentication state for the plugin. Retrieve any internal state that represents the authenticated plugin. This should not fetch any new data if it is not present. :returns: a string that can be stored or None if there is no auth state present in the plugin. This string can be reloaded with set_auth_state to set the same authentication. :rtype: str or None if no auth present. """ if self.auth_ref: data = {'auth_token': self.auth_ref.auth_token, 'body': self.auth_ref._data} return json.dumps(data) def set_auth_state(self, data): """Install existing authentication state for a plugin. Take the output of get_auth_state and install that authentication state into the current authentication plugin. """ if data: auth_data = json.loads(data) self.auth_ref = access.create(body=auth_data['body'], auth_token=auth_data['auth_token']) else: self.auth_ref = None ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2887943 keystoneauth1-4.4.0/keystoneauth1/identity/generic/0000775000175000017500000000000000000000000022417 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/generic/__init__.py0000664000175000017500000000151200000000000024527 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.identity.generic.base import BaseGenericPlugin # noqa from keystoneauth1.identity.generic.password import Password # noqa from keystoneauth1.identity.generic.token import Token # noqa __all__ = ('BaseGenericPlugin', 'Password', 'Token', ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/generic/base.py0000664000175000017500000002170100000000000023704 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc import six import six.moves.urllib.parse as urlparse from keystoneauth1 import _utils as utils from keystoneauth1 import discover from keystoneauth1 import exceptions from keystoneauth1.identity import base LOG = utils.get_logger(__name__) @six.add_metaclass(abc.ABCMeta) class BaseGenericPlugin(base.BaseIdentityPlugin): """An identity plugin that is not version dependent. Internally we will construct a version dependent plugin with the resolved URL and then proxy all calls from the base plugin to the versioned one. """ def __init__(self, auth_url, tenant_id=None, tenant_name=None, project_id=None, project_name=None, project_domain_id=None, project_domain_name=None, domain_id=None, domain_name=None, system_scope=None, trust_id=None, default_domain_id=None, default_domain_name=None, reauthenticate=True): super(BaseGenericPlugin, self).__init__(auth_url=auth_url, reauthenticate=reauthenticate) self._project_id = project_id or tenant_id self._project_name = project_name or tenant_name self._project_domain_id = project_domain_id self._project_domain_name = project_domain_name self._domain_id = domain_id self._domain_name = domain_name self._system_scope = system_scope self._trust_id = trust_id self._default_domain_id = default_domain_id self._default_domain_name = default_domain_name self._plugin = None @abc.abstractmethod def create_plugin(self, session, version, url, raw_status=None): """Create a plugin from the given parameters. This function will be called multiple times with the version and url of a potential endpoint. If a plugin can be constructed that fits the params then it should return it. If not return None and then another call will be made with other available URLs. :param session: A session object. :type session: keystoneauth1.session.Session :param tuple version: A tuple of the API version at the URL. :param str url: The base URL for this version. :param str raw_status: The status that was in the discovery field. :returns: A plugin that can match the parameters or None if nothing. """ return None @property def _has_domain_scope(self): """Are there domain parameters. Domain parameters are v3 only so returns if any are set. :returns: True if a domain parameter is set, false otherwise. """ return any([self._domain_id, self._domain_name, self._project_domain_id, self._project_domain_name]) @property def _v2_params(self): """Return the parameters that are common to v2 plugins.""" return {'trust_id': self._trust_id, 'tenant_id': self._project_id, 'tenant_name': self._project_name, 'reauthenticate': self.reauthenticate} @property def _v3_params(self): """Return the parameters that are common to v3 plugins.""" return {'trust_id': self._trust_id, 'system_scope': self._system_scope, 'project_id': self._project_id, 'project_name': self._project_name, 'project_domain_id': self.project_domain_id, 'project_domain_name': self.project_domain_name, 'domain_id': self._domain_id, 'domain_name': self._domain_name, 'reauthenticate': self.reauthenticate} @property def project_domain_id(self): return self._project_domain_id or self._default_domain_id @project_domain_id.setter def project_domain_id(self, value): self._project_domain_id = value @property def project_domain_name(self): return self._project_domain_name or self._default_domain_name @project_domain_name.setter def project_domain_name(self, value): self._project_domain_name = value def _do_create_plugin(self, session): plugin = None try: disc = self.get_discovery(session, self.auth_url, authenticated=False) except (exceptions.DiscoveryFailure, exceptions.HttpError, exceptions.SSLError, exceptions.ConnectionError) as e: LOG.warning('Failed to discover available identity versions when ' 'contacting %s. Attempting to parse version from URL.', self.auth_url) url_parts = urlparse.urlparse(self.auth_url) path = url_parts.path.lower() if path.startswith('/v2.0'): if self._has_domain_scope: raise exceptions.DiscoveryFailure( 'Cannot use v2 authentication with domain scope') plugin = self.create_plugin(session, (2, 0), self.auth_url) elif path.startswith('/v3'): plugin = self.create_plugin(session, (3, 0), self.auth_url) else: raise exceptions.DiscoveryFailure( 'Could not find versioned identity endpoints when ' 'attempting to authenticate. Please check that your ' 'auth_url is correct. %s' % e) else: # NOTE(jamielennox): version_data is always in oldest to newest # order. This is fine normally because we explicitly skip v2 below # if there is domain data present. With default_domain params # though we want a v3 plugin if available and fall back to v2 so we # have to process in reverse order. FIXME(jamielennox): if we ever # go for another version we should reverse this logic as we always # want to favour the newest available version. reverse = self._default_domain_id or self._default_domain_name disc_data = disc.version_data(reverse=bool(reverse)) v2_with_domain_scope = False for data in disc_data: version = data['version'] if (discover.version_match((2,), version) and self._has_domain_scope): # NOTE(jamielennox): if there are domain parameters there # is no point even trying against v2 APIs. v2_with_domain_scope = True continue plugin = self.create_plugin(session, version, data['url'], raw_status=data['raw_status']) if plugin: break if not plugin and v2_with_domain_scope: raise exceptions.DiscoveryFailure( 'Cannot use v2 authentication with domain scope') if plugin: return plugin # so there were no URLs that i could use for auth of any version. raise exceptions.DiscoveryFailure( 'Could not find versioned identity endpoints when attempting ' 'to authenticate. Please check that your auth_url is correct.') def get_auth_ref(self, session, **kwargs): if not self._plugin: self._plugin = self._do_create_plugin(session) return self._plugin.get_auth_ref(session, **kwargs) def get_cache_id_elements(self, _implemented=False): # NOTE(jamielennox): implemented here is just a way to make sure that # something overrides this method. We don't want the base # implementation to respond with a dict without the subclass modifying # it to add their own data in case the subclass doesn't support caching if not _implemented: raise NotImplementedError() return {'auth_url': self.auth_url, 'project_id': self._project_id, 'project_name': self._project_name, 'project_domain_id': self.project_domain_id, 'project_domain_name': self.project_domain_name, 'domain_id': self._domain_id, 'domain_name': self._domain_name, 'trust_id': self._trust_id} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/generic/password.py0000664000175000017500000000656700000000000024651 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import discover from keystoneauth1.identity.generic import base from keystoneauth1.identity import v2 from keystoneauth1.identity import v3 class Password(base.BaseGenericPlugin): """A common user/password authentication plugin. :param string username: Username for authentication. :param string user_id: User ID for authentication. :param string password: Password for authentication. :param string user_domain_id: User's domain ID for authentication. :param string user_domain_name: User's domain name for authentication. """ def __init__(self, auth_url, username=None, user_id=None, password=None, user_domain_id=None, user_domain_name=None, **kwargs): super(Password, self).__init__(auth_url=auth_url, **kwargs) self._username = username self._user_id = user_id self._password = password self._user_domain_id = user_domain_id self._user_domain_name = user_domain_name def create_plugin(self, session, version, url, raw_status=None): if discover.version_match((2,), version): if self._user_domain_id or self._user_domain_name: return None return v2.Password(auth_url=url, user_id=self._user_id, username=self._username, password=self._password, **self._v2_params) elif discover.version_match((3,), version): u_domain_id = self._user_domain_id or self._default_domain_id u_domain_name = self._user_domain_name or self._default_domain_name return v3.Password(auth_url=url, user_id=self._user_id, username=self._username, user_domain_id=u_domain_id, user_domain_name=u_domain_name, password=self._password, **self._v3_params) @property def user_domain_id(self): return self._user_domain_id or self._default_domain_id @user_domain_id.setter def user_domain_id(self, value): self._user_domain_id = value @property def user_domain_name(self): return self._user_domain_name or self._default_domain_name @user_domain_name.setter def user_domain_name(self, value): self._user_domain_name = value def get_cache_id_elements(self): elements = super(Password, self).get_cache_id_elements( _implemented=True) elements['username'] = self._username elements['user_id'] = self._user_id elements['password'] = self._password elements['user_domain_id'] = self.user_domain_id elements['user_domain_name'] = self.user_domain_name return elements ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/generic/token.py0000664000175000017500000000266700000000000024124 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import discover from keystoneauth1.identity.generic import base from keystoneauth1.identity import v2 from keystoneauth1.identity import v3 class Token(base.BaseGenericPlugin): """Generic token auth plugin. :param string token: Token for authentication. """ def __init__(self, auth_url, token=None, **kwargs): super(Token, self).__init__(auth_url, **kwargs) self._token = token def create_plugin(self, session, version, url, raw_status=None): if discover.version_match((2,), version): return v2.Token(url, self._token, **self._v2_params) elif discover.version_match((3,), version): return v3.Token(url, self._token, **self._v3_params) def get_cache_id_elements(self): elements = super(Token, self).get_cache_id_elements(_implemented=True) elements['token'] = self._token return elements ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/v2.py0000664000175000017500000001424200000000000021707 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc import six from keystoneauth1 import _utils as utils from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1.identity import base _logger = utils.get_logger(__name__) @six.add_metaclass(abc.ABCMeta) class Auth(base.BaseIdentityPlugin): """Identity V2 Authentication Plugin. :param string auth_url: Identity service endpoint for authorization. :param string trust_id: Trust ID for trust scoping. :param string tenant_id: Tenant ID for project scoping. :param string tenant_name: Tenant name for project scoping. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True """ def __init__(self, auth_url, trust_id=None, tenant_id=None, tenant_name=None, reauthenticate=True): super(Auth, self).__init__(auth_url=auth_url, reauthenticate=reauthenticate) self.trust_id = trust_id self.tenant_id = tenant_id self.tenant_name = tenant_name def get_auth_ref(self, session, **kwargs): headers = {'Accept': 'application/json'} url = self.auth_url.rstrip('/') + '/tokens' params = {'auth': self.get_auth_data(headers)} if self.tenant_id: params['auth']['tenantId'] = self.tenant_id elif self.tenant_name: params['auth']['tenantName'] = self.tenant_name if self.trust_id: params['auth']['trust_id'] = self.trust_id _logger.debug('Making authentication request to %s', url) resp = session.post(url, json=params, headers=headers, authenticated=False, log=False) try: resp_data = resp.json() except ValueError: raise exceptions.InvalidResponse(response=resp) if 'access' not in resp_data: raise exceptions.InvalidResponse(response=resp) return access.AccessInfoV2(resp_data) @abc.abstractmethod def get_auth_data(self, headers=None): """Return the authentication section of an auth plugin. :param dict headers: The headers that will be sent with the auth request if a plugin needs to add to them. :return: A dict of authentication data for the auth type. :rtype: dict """ @property def has_scope_parameters(self): """Return true if parameters can be used to create a scoped token.""" return self.tenant_id or self.tenant_name or self.trust_id _NOT_PASSED = object() class Password(Auth): """A plugin for authenticating with a username and password. A username or user_id must be provided. :param string auth_url: Identity service endpoint for authorization. :param string username: Username for authentication. :param string password: Password for authentication. :param string user_id: User ID for authentication. :param string trust_id: Trust ID for trust scoping. :param string tenant_id: Tenant ID for tenant scoping. :param string tenant_name: Tenant name for tenant scoping. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True :raises TypeError: if a user_id or username is not provided. """ def __init__(self, auth_url, username=_NOT_PASSED, password=None, user_id=_NOT_PASSED, **kwargs): super(Password, self).__init__(auth_url, **kwargs) if username is _NOT_PASSED and user_id is _NOT_PASSED: msg = 'You need to specify either a username or user_id' raise TypeError(msg) if username is _NOT_PASSED: username = None if user_id is _NOT_PASSED: user_id = None self.user_id = user_id self.username = username self.password = password def get_auth_data(self, headers=None): auth = {'password': self.password} if self.username: auth['username'] = self.username elif self.user_id: auth['userId'] = self.user_id return {'passwordCredentials': auth} def get_cache_id_elements(self): return {'username': self.username, 'user_id': self.user_id, 'password': self.password, 'auth_url': self.auth_url, 'tenant_id': self.tenant_id, 'tenant_name': self.tenant_name, 'trust_id': self.trust_id} class Token(Auth): """A plugin for authenticating with an existing token. :param string auth_url: Identity service endpoint for authorization. :param string token: Existing token for authentication. :param string tenant_id: Tenant ID for tenant scoping. :param string tenant_name: Tenant name for tenant scoping. :param string trust_id: Trust ID for trust scoping. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True """ def __init__(self, auth_url, token, **kwargs): super(Token, self).__init__(auth_url, **kwargs) self.token = token def get_auth_data(self, headers=None): if headers is not None: headers['X-Auth-Token'] = self.token return {'token': {'id': self.token}} def get_cache_id_elements(self): return {'token': self.token, 'auth_url': self.auth_url, 'tenant_id': self.tenant_id, 'tenant_name': self.tenant_name, 'trust_id': self.trust_id} ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2927942 keystoneauth1-4.4.0/keystoneauth1/identity/v3/0000775000175000017500000000000000000000000021333 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/v3/__init__.py0000664000175000017500000000341700000000000023451 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # flake8: noqa: F405 from keystoneauth1.identity.v3.application_credential import * # noqa from keystoneauth1.identity.v3.base import * # noqa from keystoneauth1.identity.v3.federation import * # noqa from keystoneauth1.identity.v3.k2k import * # noqa from keystoneauth1.identity.v3.multi_factor import * # noqa from keystoneauth1.identity.v3.oidc import * # noqa from keystoneauth1.identity.v3.password import * # noqa from keystoneauth1.identity.v3.receipt import * # noqa from keystoneauth1.identity.v3.token import * # noqa from keystoneauth1.identity.v3.totp import * # noqa from keystoneauth1.identity.v3.tokenless_auth import * # noqa __all__ = ('ApplicationCredential', 'ApplicationCredentialMethod', 'Auth', 'AuthConstructor', 'AuthMethod', 'BaseAuth', 'FederationBaseAuth', 'Keystone2Keystone', 'Password', 'PasswordMethod', 'Token', 'TokenMethod', 'OidcAccessToken', 'OidcAuthorizationCode', 'OidcClientCredentials', 'OidcPassword', 'TOTPMethod', 'TOTP', 'TokenlessAuth', 'ReceiptMethod', 'MultiFactor', ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/v3/application_credential.py0000664000175000017500000000770400000000000026412 0ustar00zuulzuul00000000000000# Copyright 2018 SUSE Linux GmbH # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.identity.v3 import base __all__ = ('ApplicationCredentialMethod', 'ApplicationCredential') class ApplicationCredentialMethod(base.AuthMethod): """Construct a User/Passcode based authentication method. :param string application_credential_secret: Application credential secret. :param string application_credential_id: Application credential id. :param string application_credential_name: The name of the application credential, if an ID is not provided. :param string username: Username for authentication, if an application credential ID is not provided. :param string user_id: User ID for authentication, if an application credential ID is not provided. :param string user_domain_id: User's domain ID for authentication, if an application credential ID is not provided. :param string user_domain_name: User's domain name for authentication, if an application credential ID is not provided. """ _method_parameters = ['application_credential_secret', 'application_credential_id', 'application_credential_name', 'user_id', 'username', 'user_domain_id', 'user_domain_name'] def get_auth_data(self, session, auth, headers, **kwargs): auth_data = {'secret': self.application_credential_secret} if self.application_credential_id: auth_data['id'] = self.application_credential_id else: auth_data['name'] = self.application_credential_name auth_data['user'] = {} if self.user_id: auth_data['user']['id'] = self.user_id elif self.username: auth_data['user']['name'] = self.username if self.user_domain_id: auth_data['user']['domain'] = {'id': self.user_domain_id} elif self.user_domain_name: auth_data['user']['domain'] = { 'name': self.user_domain_name} return 'application_credential', auth_data def get_cache_id_elements(self): return dict(('application_credential_%s' % p, getattr(self, p)) for p in self._method_parameters) class ApplicationCredential(base.AuthConstructor): """A plugin for authenticating with an application credential. :param string auth_url: Identity service endpoint for authentication. :param string application_credential_secret: Application credential secret. :param string application_credential_id: Application credential ID. :param string application_credential_name: Application credential name. :param string username: Username for authentication. :param string user_id: User ID for authentication. :param string user_domain_id: User's domain ID for authentication. :param string user_domain_name: User's domain name for authentication. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True """ _auth_method_class = ApplicationCredentialMethod ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/v3/base.py0000664000175000017500000003024600000000000022624 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc import json import six from keystoneauth1 import _utils as utils from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1.identity import base _logger = utils.get_logger(__name__) __all__ = ('Auth', 'AuthMethod', 'AuthConstructor', 'BaseAuth') @six.add_metaclass(abc.ABCMeta) class BaseAuth(base.BaseIdentityPlugin): """Identity V3 Authentication Plugin. :param string auth_url: Identity service endpoint for authentication. :param string trust_id: Trust ID for trust scoping. :param string system_scope: System information to scope to. :param string domain_id: Domain ID for domain scoping. :param string domain_name: Domain name for domain scoping. :param string project_id: Project ID for project scoping. :param string project_name: Project name for project scoping. :param string project_domain_id: Project's domain ID for project. :param string project_domain_name: Project's domain name for project. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True :param bool include_catalog: Include the service catalog in the returned token. (optional) default True. """ def __init__(self, auth_url, trust_id=None, system_scope=None, domain_id=None, domain_name=None, project_id=None, project_name=None, project_domain_id=None, project_domain_name=None, reauthenticate=True, include_catalog=True): super(BaseAuth, self).__init__(auth_url=auth_url, reauthenticate=reauthenticate) self.trust_id = trust_id self.system_scope = system_scope self.domain_id = domain_id self.domain_name = domain_name self.project_id = project_id self.project_name = project_name self.project_domain_id = project_domain_id self.project_domain_name = project_domain_name self.include_catalog = include_catalog @property def token_url(self): """The full URL where we will send authentication data.""" return '%s/auth/tokens' % self.auth_url.rstrip('/') @abc.abstractmethod def get_auth_ref(self, session, **kwargs): return None @property def has_scope_parameters(self): """Return true if parameters can be used to create a scoped token.""" return (self.domain_id or self.domain_name or self.project_id or self.project_name or self.trust_id or self.system_scope) class Auth(BaseAuth): """Identity V3 Authentication Plugin. :param string auth_url: Identity service endpoint for authentication. :param list auth_methods: A collection of methods to authenticate with. :param string trust_id: Trust ID for trust scoping. :param string domain_id: Domain ID for domain scoping. :param string domain_name: Domain name for domain scoping. :param string project_id: Project ID for project scoping. :param string project_name: Project name for project scoping. :param string project_domain_id: Project's domain ID for project. :param string project_domain_name: Project's domain name for project. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True :param bool include_catalog: Include the service catalog in the returned token. (optional) default True. :param bool unscoped: Force the return of an unscoped token. This will make the keystone server return an unscoped token even if a default_project_id is set for this user. """ def __init__(self, auth_url, auth_methods, **kwargs): self.unscoped = kwargs.pop('unscoped', False) super(Auth, self).__init__(auth_url=auth_url, **kwargs) self.auth_methods = auth_methods def add_method(self, method): """Add an additional initialized AuthMethod instance.""" self.auth_methods.append(method) def get_auth_ref(self, session, **kwargs): headers = {'Accept': 'application/json'} body = {'auth': {'identity': {}}} ident = body['auth']['identity'] rkwargs = {} for method in self.auth_methods: name, auth_data = method.get_auth_data( session, self, headers, request_kwargs=rkwargs) # NOTE(adriant): Methods like ReceiptMethod don't # want anything added to the request data, so they # explicitly return None, which we check for. if name: ident.setdefault('methods', []).append(name) ident[name] = auth_data if not ident: raise exceptions.AuthorizationFailure( 'Authentication method required (e.g. password)') mutual_exclusion = [bool(self.domain_id or self.domain_name), bool(self.project_id or self.project_name), bool(self.trust_id), bool(self.unscoped)] if sum(mutual_exclusion) > 1: raise exceptions.AuthorizationFailure( message='Authentication cannot be scoped to multiple' ' targets. Pick one of: project, domain, ' 'trust or unscoped') if self.domain_id: body['auth']['scope'] = {'domain': {'id': self.domain_id}} elif self.domain_name: body['auth']['scope'] = {'domain': {'name': self.domain_name}} elif self.project_id: body['auth']['scope'] = {'project': {'id': self.project_id}} elif self.project_name: scope = body['auth']['scope'] = {'project': {}} scope['project']['name'] = self.project_name if self.project_domain_id: scope['project']['domain'] = {'id': self.project_domain_id} elif self.project_domain_name: scope['project']['domain'] = {'name': self.project_domain_name} elif self.trust_id: body['auth']['scope'] = {'OS-TRUST:trust': {'id': self.trust_id}} elif self.unscoped: body['auth']['scope'] = 'unscoped' elif self.system_scope: # NOTE(lbragstad): Right now it's only possible to have role # assignments on the entire system. In the future that might change # so that users and groups can have roles on parts of the system, # like a specific service in a specific region. If that happens, # this will have to be accounted for here. Until then we'll only # support scoping to the entire system. if self.system_scope == 'all': body['auth']['scope'] = {'system': {'all': True}} token_url = self.token_url if not self.auth_url.rstrip('/').endswith('v3'): token_url = '%s/v3/auth/tokens' % self.auth_url.rstrip('/') # NOTE(jamielennox): we add nocatalog here rather than in token_url # directly as some federation plugins require the base token_url if not self.include_catalog: token_url += '?nocatalog' _logger.debug('Making authentication request to %s', token_url) resp = session.post(token_url, json=body, headers=headers, authenticated=False, log=False, **rkwargs) try: _logger.debug(json.dumps(resp.json())) resp_data = resp.json() except ValueError: raise exceptions.InvalidResponse(response=resp) if 'token' not in resp_data: raise exceptions.InvalidResponse(response=resp) return access.AccessInfoV3(auth_token=resp.headers['X-Subject-Token'], body=resp_data) def get_cache_id_elements(self): if not self.auth_methods: return None params = {'auth_url': self.auth_url, 'domain_id': self.domain_id, 'domain_name': self.domain_name, 'project_id': self.project_id, 'project_name': self.project_name, 'project_domain_id': self.project_domain_id, 'project_domain_name': self.project_domain_name, 'trust_id': self.trust_id} for method in self.auth_methods: try: elements = method.get_cache_id_elements() except NotImplementedError: return None params.update(elements) return params @six.add_metaclass(abc.ABCMeta) class AuthMethod(object): """One part of a V3 Authentication strategy. V3 Tokens allow multiple methods to be presented when authentication against the server. Each one of these methods is implemented by an AuthMethod. Note: When implementing an AuthMethod use the method_parameters and do not use positional arguments. Otherwise they can't be picked up by the factory method and don't work as well with AuthConstructors. """ _method_parameters = [] def __init__(self, **kwargs): for param in self._method_parameters: setattr(self, param, kwargs.pop(param, None)) if kwargs: msg = "Unexpected Attributes: %s" % ", ".join(kwargs.keys()) raise AttributeError(msg) @classmethod def _extract_kwargs(cls, kwargs): """Remove parameters related to this method from other kwargs.""" return dict([(p, kwargs.pop(p, None)) for p in cls._method_parameters]) @abc.abstractmethod def get_auth_data(self, session, auth, headers, **kwargs): """Return the authentication section of an auth plugin. :param session: The communication session. :type session: keystoneauth1.session.Session :param base.Auth auth: The auth plugin calling the method. :param dict headers: The headers that will be sent with the auth request if a plugin needs to add to them. :return: The identifier of this plugin and a dict of authentication data for the auth type. :rtype: tuple(string, dict) """ def get_cache_id_elements(self): """Get the elements for this auth method that make it unique. These elements will be used as part of the :py:meth:`keystoneauth1.plugin.BaseIdentityPlugin.get_cache_id` to allow caching of the auth plugin. Plugins should override this if they want to allow caching of their state. To avoid collision or overrides the keys of the returned dictionary should be prefixed with the plugin identifier. For example the password plugin returns its username value as 'password_username'. """ raise NotImplementedError() @six.add_metaclass(abc.ABCMeta) class AuthConstructor(Auth): """Abstract base class for creating an Auth Plugin. The Auth Plugin created contains only one authentication method. This is generally the required usage. An AuthConstructor creates an AuthMethod based on the method's arguments and the auth_method_class defined by the plugin. It then creates the auth plugin with only that authentication method. """ _auth_method_class = None def __init__(self, auth_url, *args, **kwargs): method_kwargs = self._auth_method_class._extract_kwargs(kwargs) method = self._auth_method_class(*args, **method_kwargs) super(AuthConstructor, self).__init__(auth_url, [method], **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/v3/federation.py0000664000175000017500000001011600000000000024024 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc import six from keystoneauth1.identity.v3 import base from keystoneauth1.identity.v3 import token __all__ = ('FederationBaseAuth',) @six.add_metaclass(abc.ABCMeta) class _Rescoped(base.BaseAuth): """A plugin that is always going to go through a rescope process. The original keystone plugins could simply pass a project or domain to along with the credentials and get a scoped token. For federation, K2K and newer mechanisms we always get an unscoped token first and then rescope. This is currently not public as it's generally an abstraction of a flow used by plugins within keystoneauth1. It also cannot go in base as it depends on token.Token for rescoping which would create a circular dependency. """ rescoping_plugin = token.Token def _get_scoping_data(self): return {'trust_id': self.trust_id, 'domain_id': self.domain_id, 'domain_name': self.domain_name, 'project_id': self.project_id, 'project_name': self.project_name, 'project_domain_id': self.project_domain_id, 'project_domain_name': self.project_domain_name} def get_auth_ref(self, session, **kwargs): """Authenticate retrieve token information. This is a multi-step process where a client does federated authn receives an unscoped token. If an unscoped token is successfully received and scoping information is present then the token is rescoped to that target. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :returns: a token data representation :rtype: :py:class:`keystoneauth1.access.AccessInfo` """ auth_ref = self.get_unscoped_auth_ref(session) scoping = self._get_scoping_data() if any(scoping.values()): token_plugin = self.rescoping_plugin(self.auth_url, token=auth_ref.auth_token, **scoping) auth_ref = token_plugin.get_auth_ref(session) return auth_ref @abc.abstractmethod def get_unscoped_auth_ref(self, session, **kwargs): """Fetch unscoped federated token.""" class FederationBaseAuth(_Rescoped): """Federation authentication plugin. :param auth_url: URL of the Identity Service :type auth_url: string :param identity_provider: name of the Identity Provider the client will authenticate against. This parameter will be used to build a dynamic URL used to obtain unscoped OpenStack token. :type identity_provider: string :param protocol: name of the protocol the client will authenticate against. :type protocol: string """ def __init__(self, auth_url, identity_provider, protocol, **kwargs): super(FederationBaseAuth, self).__init__(auth_url=auth_url, **kwargs) self.identity_provider = identity_provider self.protocol = protocol @property def federated_token_url(self): """Full URL where authorization data is sent.""" values = { 'host': self.auth_url.rstrip('/'), 'identity_provider': self.identity_provider, 'protocol': self.protocol } url = ("%(host)s/OS-FEDERATION/identity_providers/" "%(identity_provider)s/protocols/%(protocol)s/auth") url = url % values return url ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/v3/k2k.py0000664000175000017500000001530400000000000022377 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import six from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1.identity.v3 import federation from keystoneauth1 import plugin __all__ = ('Keystone2Keystone',) class Keystone2Keystone(federation._Rescoped): """Plugin to execute the Keystone to Keyestone authentication flow. In this plugin, an ECP wrapped SAML assertion provided by a keystone Identity Provider (IdP) is used to request an OpenStack unscoped token from a keystone Service Provider (SP). :param base_plugin: Auth plugin already authenticated against the keystone IdP. :type base_plugin: keystoneauth1.identity.v3.base.BaseAuth :param service_provider: The Service Provider ID as returned by ServiceProviderManager.list() :type service_provider: str """ REQUEST_ECP_URL = '/auth/OS-FEDERATION/saml2/ecp' """Path where the ECP wrapped SAML assertion should be presented to the Keystone Service Provider.""" HTTP_MOVED_TEMPORARILY = 302 HTTP_SEE_OTHER = 303 def __init__(self, base_plugin, service_provider, **kwargs): super(Keystone2Keystone, self).__init__(auth_url=None, **kwargs) self._local_cloud_plugin = base_plugin self._sp_id = service_provider @classmethod def _remote_auth_url(cls, auth_url): """Return auth_url of the remote Keystone Service Provider. Remote cloud's auth_url is an endpoint for getting federated unscoped token, typically that would be ``https://remote.example.com:5000/v3/OS-FEDERATION/identity_providers/ /protocols//auth``. However we need to generate a real auth_url, used for token scoping. This function assumes there are static values today in the remote auth_url stored in the Service Provider attribute and those can be used as a delimiter. If the sp_auth_url doesn't comply with standard federation auth url the function will simply return whole string. :param auth_url: auth_url of the remote cloud :type auth_url: str :returns: auth_url of remote cloud where a token can be validated or scoped. :rtype: str """ PATTERN = '/OS-FEDERATION/' idx = auth_url.index(PATTERN) if PATTERN in auth_url else len(auth_url) return auth_url[:idx] def _get_ecp_assertion(self, session): body = { 'auth': { 'identity': { 'methods': ['token'], 'token': { 'id': self._local_cloud_plugin.get_token(session) } }, 'scope': { 'service_provider': { 'id': self._sp_id } } } } endpoint_filter = {'version': (3, 0), 'interface': plugin.AUTH_INTERFACE} headers = {'Accept': 'application/json'} resp = session.post(self.REQUEST_ECP_URL, json=body, auth=self._local_cloud_plugin, endpoint_filter=endpoint_filter, headers=headers, authenticated=False, raise_exc=False) # NOTE(marek-denis): I am not sure whether disabling exceptions in the # Session object and testing if resp.ok is sufficient. An alternative # would be catching locally all exceptions and reraising with custom # warning. if not resp.ok: msg = ("Error while requesting ECP wrapped assertion: response " "exit code: %(status_code)d, reason: %(err)s") msg = msg % {'status_code': resp.status_code, 'err': resp.reason} raise exceptions.AuthorizationFailure(msg) if not resp.text: raise exceptions.InvalidResponse(resp) return six.text_type(resp.text) def _send_service_provider_ecp_authn_response(self, session, sp_url, sp_auth_url): """Present ECP wrapped SAML assertion to the keystone SP. The assertion is issued by the keystone IdP and it is targeted to the keystone that will serve as Service Provider. :param session: a session object to send out HTTP requests. :param sp_url: URL where the ECP wrapped SAML assertion will be presented to the keystone SP. Usually, something like: https://sp.com/Shibboleth.sso/SAML2/ECP :type sp_url: str :param sp_auth_url: Federated authentication URL of the keystone SP. It is specified by IdP, for example: https://sp.com/v3/OS-FEDERATION/identity_providers/ idp_id/protocols/protocol_id/auth :type sp_auth_url: str """ response = session.post( sp_url, headers={'Content-Type': 'application/vnd.paos+xml'}, data=self._get_ecp_assertion(session), authenticated=False, redirect=False) # Don't follow HTTP specs - after the HTTP 302/303 response don't # repeat the call directed to the Location URL. In this case, this is # an indication that SAML2 session is now active and protected resource # can be accessed. if response.status_code in (self.HTTP_MOVED_TEMPORARILY, self.HTTP_SEE_OTHER): response = session.get( sp_auth_url, headers={'Content-Type': 'application/vnd.paos+xml'}, authenticated=False) return response def get_unscoped_auth_ref(self, session, **kwargs): sp_auth_url = self._local_cloud_plugin.get_sp_auth_url( session, self._sp_id) sp_url = self._local_cloud_plugin.get_sp_url(session, self._sp_id) self.auth_url = self._remote_auth_url(sp_auth_url) response = self._send_service_provider_ecp_authn_response( session, sp_url, sp_auth_url) return access.create(resp=response) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/v3/multi_factor.py0000664000175000017500000000536400000000000024405 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.identity.v3 import base from keystoneauth1 import loading __all__ = ('MultiFactor', ) class MultiFactor(base.Auth): """A plugin for authenticating with multiple auth methods. :param string auth_url: Identity service endpoint for authentication. :param string auth_methods: names of the methods to authenticate with. :param string trust_id: Trust ID for trust scoping. :param string system_scope: System information to scope to. :param string domain_id: Domain ID for domain scoping. :param string domain_name: Domain name for domain scoping. :param string project_id: Project ID for project scoping. :param string project_name: Project name for project scoping. :param string project_domain_id: Project's domain ID for project. :param string project_domain_name: Project's domain name for project. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True Also accepts various keyword args based on which methods are specified. """ def __init__(self, auth_url, auth_methods, **kwargs): method_instances = [] method_keys = set() for method in auth_methods: # Using the loaders we pull the related auth method class method_class = loading.get_plugin_loader( method).plugin_class._auth_method_class # We build some new kwargs for the method from required parameters method_kwargs = {} for key in method_class._method_parameters: # we add them to method_keys to pop later from global kwargs # rather than here as other methods may need them too method_keys.add(key) method_kwargs[key] = kwargs.get(key, None) # We initialize the method class using just required kwargs method_instances.append(method_class(**method_kwargs)) # We now pop all the keys used for methods as otherwise they get passed # to the super class and throw errors for key in method_keys: kwargs.pop(key, None) super(MultiFactor, self).__init__(auth_url, method_instances, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/v3/oidc.py0000664000175000017500000004525400000000000022635 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc import warnings import six from keystoneauth1 import _utils as utils from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1.identity.v3 import federation _logger = utils.get_logger(__name__) __all__ = ('OidcAuthorizationCode', 'OidcClientCredentials', 'OidcPassword', 'OidcAccessToken') @six.add_metaclass(abc.ABCMeta) class _OidcBase(federation.FederationBaseAuth): """Base class for different OpenID Connect based flows. The OpenID Connect specification can be found at:: ``http://openid.net/specs/openid-connect-core-1_0.html`` """ grant_type = None def __init__(self, auth_url, identity_provider, protocol, client_id, client_secret, access_token_type, scope="openid profile", access_token_endpoint=None, discovery_endpoint=None, grant_type=None, **kwargs): """The OpenID Connect plugin expects the following. :param auth_url: URL of the Identity Service :type auth_url: string :param identity_provider: Name of the Identity Provider the client will authenticate against :type identity_provider: string :param protocol: Protocol name as configured in keystone :type protocol: string :param client_id: OAuth 2.0 Client ID :type client_id: string :param client_secret: OAuth 2.0 Client Secret :type client_secret: string :param access_token_type: OAuth 2.0 Authorization Server Introspection token type, it is used to decide which type of token will be used when processing token introspection. Valid values are: "access_token" or "id_token" :type access_token_type: string :param access_token_endpoint: OpenID Connect Provider Token Endpoint, for example: https://localhost:8020/oidc/OP/token Note that if a discovery document is provided this value will override the discovered one. :type access_token_endpoint: string :param discovery_endpoint: OpenID Connect Discovery Document URL, for example: https://localhost:8020/oidc/.well-known/openid-configuration :type access_token_endpoint: string :param scope: OpenID Connect scope that is requested from OP, for example: "openid profile email", defaults to "openid profile". Note that OpenID Connect specification states that "openid" must be always specified. :type scope: string """ super(_OidcBase, self).__init__(auth_url, identity_provider, protocol, **kwargs) self.client_id = client_id self.client_secret = client_secret self.discovery_endpoint = discovery_endpoint self._discovery_document = {} self.access_token_endpoint = access_token_endpoint self.access_token_type = access_token_type self.scope = scope if grant_type is not None: if grant_type != self.grant_type: raise exceptions.OidcGrantTypeMissmatch() warnings.warn("Passing grant_type as an argument has been " "deprecated as it is now defined in the plugin " "itself. You should stop passing this argument " "to the plugin, as it will be ignored, since you " "cannot pass a free text string as a grant_type. " "This argument will be dropped from the plugin in " "July 2017 or with the next major release of " "keystoneauth (3.0.0)", DeprecationWarning) def _get_discovery_document(self, session): """Get the contents of the OpenID Connect Discovery Document. This method grabs the contents of the OpenID Connect Discovery Document if a discovery_endpoint was passed to the constructor and returns it as a dict, otherwise returns an empty dict. Note that it will fetch the discovery document only once, so subsequent calls to this method will return the cached result, if any. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :returns: a python dictionary containing the discovery document if any, otherwise it will return an empty dict. :rtype: dict """ if (self.discovery_endpoint is not None and not self._discovery_document): try: resp = session.get(self.discovery_endpoint, authenticated=False) except exceptions.HttpError: _logger.error("Cannot fetch discovery document %(discovery)s" % {"discovery": self.discovery_endpoint}) raise try: self._discovery_document = resp.json() except Exception: pass if not self._discovery_document: raise exceptions.InvalidOidcDiscoveryDocument() return self._discovery_document def _get_access_token_endpoint(self, session): """Get the "token_endpoint" for the OpenID Connect flow. This method will return the correct access token endpoint to be used. If the user has explicitly passed an access_token_endpoint to the constructor that will be returned. If there is no explicit endpoint and a discovery url is provided, it will try to get it from the discovery document. If nothing is found, an exception will be raised. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :return: the endpoint to use :rtype: string or None if no endpoint is found """ if self.access_token_endpoint is not None: return self.access_token_endpoint discovery = self._get_discovery_document(session) endpoint = discovery.get("token_endpoint") if endpoint is None: raise exceptions.OidcAccessTokenEndpointNotFound() return endpoint def _get_access_token(self, session, payload): """Exchange a variety of user supplied values for an access token. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :param payload: a dict containing various OpenID Connect values, for example:: {'grant_type': 'password', 'username': self.username, 'password': self.password, 'scope': self.scope} :type payload: dict """ client_auth = (self.client_id, self.client_secret) access_token_endpoint = self._get_access_token_endpoint(session) op_response = session.post(access_token_endpoint, requests_auth=client_auth, data=payload, authenticated=False) access_token = op_response.json()[self.access_token_type] return access_token def _get_keystone_token(self, session, access_token): r"""Exchange an access token for a keystone token. By Sending the access token in an `Authorization: Bearer` header, to an OpenID Connect protected endpoint (Federated Token URL). The OpenID Connect server will use the access token to look up information about the authenticated user (this technique is called instrospection). The output of the instrospection will be an OpenID Connect Claim, that will be used against the mapping engine. Should the mapping engine succeed, a Keystone token will be presented to the user. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :param access_token: The OpenID Connect access token. :type access_token: str """ # use access token against protected URL headers = {'Authorization': 'Bearer ' + access_token} auth_response = session.post(self.federated_token_url, headers=headers, authenticated=False) return auth_response def get_unscoped_auth_ref(self, session): """Authenticate with OpenID Connect and get back claims. This is a multi-step process: 1.- An access token must be retrieved from the server. In order to do so, we need to exchange an authorization grant or refresh token with the token endpoint in order to obtain an access token. The authorization grant varies from plugin to plugin. 2.- We then exchange the access token upon accessing the protected Keystone endpoint (federated auth URL). This will trigger the OpenID Connect Provider to perform a user introspection and retrieve information (specified in the scope) about the user in the form of an OpenID Connect Claim. These claims will be sent to Keystone in the form of environment variables. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :returns: a token data representation :rtype: :py:class:`keystoneauth1.access.AccessInfoV3` """ # First of all, check if the grant type is supported discovery = self._get_discovery_document(session) grant_types = discovery.get("grant_types_supported") if (grant_types and self.grant_type is not None and self.grant_type not in grant_types): raise exceptions.OidcPluginNotSupported() # Get the payload payload = self.get_payload(session) payload.setdefault('grant_type', self.grant_type) # get an access token access_token = self._get_access_token(session, payload) response = self._get_keystone_token(session, access_token) # grab the unscoped token return access.create(resp=response) @abc.abstractmethod def get_payload(self, session): """Get the plugin specific payload for obtainin an access token. OpenID Connect supports different grant types. This method should prepare the payload that needs to be exchanged with the server in order to get an access token for the particular grant type that the plugin is implementing. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :returns: a python dictionary containing the payload to be exchanged :rtype: dict """ raise NotImplementedError() class OidcPassword(_OidcBase): """Implementation for OpenID Connect Resource Owner Password Credential.""" grant_type = "password" def __init__(self, auth_url, identity_provider, protocol, # nosec client_id, client_secret, access_token_endpoint=None, discovery_endpoint=None, access_token_type='access_token', username=None, password=None, **kwargs): """The OpenID Password plugin expects the following. :param username: Username used to authenticate :type username: string :param password: Password used to authenticate :type password: string """ super(OidcPassword, self).__init__( auth_url=auth_url, identity_provider=identity_provider, protocol=protocol, client_id=client_id, client_secret=client_secret, access_token_endpoint=access_token_endpoint, discovery_endpoint=discovery_endpoint, access_token_type=access_token_type, **kwargs) self.username = username self.password = password def get_payload(self, session): """Get an authorization grant for the "password" grant type. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :returns: a python dictionary containing the payload to be exchanged :rtype: dict """ payload = {'username': self.username, 'password': self.password, 'scope': self.scope} return payload class OidcClientCredentials(_OidcBase): """Implementation for OpenID Connect Client Credentials.""" grant_type = 'client_credentials' def __init__(self, auth_url, identity_provider, protocol, # nosec client_id, client_secret, access_token_endpoint=None, discovery_endpoint=None, access_token_type='access_token', **kwargs): """The OpenID Client Credentials expects the following. :param client_id: Client ID used to authenticate :type username: string :param client_secret: Client Secret used to authenticate :type password: string """ super(OidcClientCredentials, self).__init__( auth_url=auth_url, identity_provider=identity_provider, protocol=protocol, client_id=client_id, client_secret=client_secret, access_token_endpoint=access_token_endpoint, discovery_endpoint=discovery_endpoint, access_token_type=access_token_type, **kwargs) def get_payload(self, session): """Get an authorization grant for the client credentials grant type. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :returns: a python dictionary containing the payload to be exchanged :rtype: dict """ payload = {'scope': self.scope} return payload class OidcAuthorizationCode(_OidcBase): """Implementation for OpenID Connect Authorization Code.""" grant_type = 'authorization_code' def __init__(self, auth_url, identity_provider, protocol, # nosec client_id, client_secret, access_token_endpoint=None, discovery_endpoint=None, access_token_type='access_token', redirect_uri=None, code=None, **kwargs): """The OpenID Authorization Code plugin expects the following. :param redirect_uri: OpenID Connect Client Redirect URL :type redirect_uri: string :param code: OAuth 2.0 Authorization Code :type code: string """ super(OidcAuthorizationCode, self).__init__( auth_url=auth_url, identity_provider=identity_provider, protocol=protocol, client_id=client_id, client_secret=client_secret, access_token_endpoint=access_token_endpoint, discovery_endpoint=discovery_endpoint, access_token_type=access_token_type, **kwargs) self.redirect_uri = redirect_uri self.code = code def get_payload(self, session): """Get an authorization grant for the "authorization_code" grant type. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :returns: a python dictionary containing the payload to be exchanged :rtype: dict """ payload = {'redirect_uri': self.redirect_uri, 'code': self.code} return payload class OidcAccessToken(_OidcBase): """Implementation for OpenID Connect access token reuse.""" def __init__(self, auth_url, identity_provider, protocol, access_token, **kwargs): """The OpenID Connect plugin based on the Access Token. It expects the following: :param auth_url: URL of the Identity Service :type auth_url: string :param identity_provider: Name of the Identity Provider the client will authenticate against :type identity_provider: string :param protocol: Protocol name as configured in keystone :type protocol: string :param access_token: OpenID Connect Access token :type access_token: string """ super(OidcAccessToken, self).__init__(auth_url, identity_provider, protocol, client_id=None, client_secret=None, access_token_endpoint=None, access_token_type=None, **kwargs) self.access_token = access_token def get_payload(self, session): """OidcAccessToken does not require a payload.""" return {} def get_unscoped_auth_ref(self, session): """Authenticate with OpenID Connect and get back claims. We exchange the access token upon accessing the protected Keystone endpoint (federated auth URL). This will trigger the OpenID Connect Provider to perform a user introspection and retrieve information (specified in the scope) about the user in the form of an OpenID Connect Claim. These claims will be sent to Keystone in the form of environment variables. :param session: a session object to send out HTTP requests. :type session: keystoneauth1.session.Session :returns: a token data representation :rtype: :py:class:`keystoneauth1.access.AccessInfoV3` """ response = self._get_keystone_token(session, self.access_token) return access.create(resp=response) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/v3/password.py0000664000175000017500000000604700000000000023556 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.identity.v3 import base __all__ = ('PasswordMethod', 'Password') class PasswordMethod(base.AuthMethod): """Construct a User/Password based authentication method. :param string password: Password for authentication. :param string username: Username for authentication. :param string user_id: User ID for authentication. :param string user_domain_id: User's domain ID for authentication. :param string user_domain_name: User's domain name for authentication. """ _method_parameters = ['user_id', 'username', 'user_domain_id', 'user_domain_name', 'password'] def get_auth_data(self, session, auth, headers, **kwargs): user = {'password': self.password} if self.user_id: user['id'] = self.user_id elif self.username: user['name'] = self.username if self.user_domain_id: user['domain'] = {'id': self.user_domain_id} elif self.user_domain_name: user['domain'] = {'name': self.user_domain_name} return 'password', {'user': user} def get_cache_id_elements(self): return dict(('password_%s' % p, getattr(self, p)) for p in self._method_parameters) class Password(base.AuthConstructor): """A plugin for authenticating with a username and password. :param string auth_url: Identity service endpoint for authentication. :param string password: Password for authentication. :param string username: Username for authentication. :param string user_id: User ID for authentication. :param string user_domain_id: User's domain ID for authentication. :param string user_domain_name: User's domain name for authentication. :param string trust_id: Trust ID for trust scoping. :param string system_scope: System information to scope to. :param string domain_id: Domain ID for domain scoping. :param string domain_name: Domain name for domain scoping. :param string project_id: Project ID for project scoping. :param string project_name: Project name for project scoping. :param string project_domain_id: Project's domain ID for project. :param string project_domain_name: Project's domain name for project. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True """ _auth_method_class = PasswordMethod ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/v3/receipt.py0000664000175000017500000000231700000000000023343 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.identity.v3 import base __all__ = ('ReceiptMethod', ) class ReceiptMethod(base.AuthMethod): """Construct an Auth plugin to continue authentication with a receipt. :param string receipt: Receipt for authentication. """ _method_parameters = ['receipt'] def get_auth_data(self, session, auth, headers, **kwargs): """Add the auth receipt to the headers. We explicitly return None to avoid being added to the request methods, or body. """ headers['Openstack-Auth-Receipt'] = self.receipt return (None, None) def get_cache_id_elements(self): return {'receipt_receipt': self.receipt} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/v3/token.py0000664000175000017500000000400100000000000023020 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.identity.v3 import base __all__ = ('TokenMethod', 'Token') class TokenMethod(base.AuthMethod): """Construct an Auth plugin to fetch a token from a token. :param string token: Token for authentication. """ _method_parameters = ['token'] def get_auth_data(self, session, auth, headers, **kwargs): headers['X-Auth-Token'] = self.token return 'token', {'id': self.token} def get_cache_id_elements(self): return {'token_token': self.token} class Token(base.AuthConstructor): """A plugin for authenticating with an existing Token. :param string auth_url: Identity service endpoint for authentication. :param string token: Token for authentication. :param string trust_id: Trust ID for trust scoping. :param string domain_id: Domain ID for domain scoping. :param string domain_name: Domain name for domain scoping. :param string project_id: Project ID for project scoping. :param string project_name: Project name for project scoping. :param string project_domain_id: Project's domain ID for project. :param string project_domain_name: Project's domain name for project. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True """ _auth_method_class = TokenMethod def __init__(self, auth_url, token, **kwargs): super(Token, self).__init__(auth_url, token=token, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/v3/tokenless_auth.py0000664000175000017500000001116700000000000024743 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc import six from keystoneauth1 import _utils as utils from keystoneauth1 import plugin LOG = utils.get_logger(__name__) @six.add_metaclass(abc.ABCMeta) class TokenlessAuth(plugin.BaseAuthPlugin): """A plugin for authenticating with Tokenless Auth. This is for Tokenless Authentication. Scoped information like domain name and project ID will be passed in the headers and token validation request will be authenticated based on the provided HTTPS certificate along with the scope information. """ def __init__(self, auth_url, domain_id=None, domain_name=None, project_id=None, project_name=None, project_domain_id=None, project_domain_name=None): """A init method for TokenlessAuth. :param string auth_url: Identity service endpoint for authentication. The URL must include a version or any request will result in a 404 NotFound error. :param string domain_id: Domain ID for domain scoping. :param string domain_name: Domain name for domain scoping. :param string project_id: Project ID for project scoping. :param string project_name: Project name for project scoping. :param string project_domain_id: Project's domain ID for project. :param string project_domain_name: Project's domain name for project. """ self.auth_url = auth_url self.domain_id = domain_id self.domain_name = domain_name self.project_id = project_id self.project_name = project_name self.project_domain_id = project_domain_id self.project_domain_name = project_domain_name def get_headers(self, session, **kwargs): """Fetch authentication headers for message. This is to override the default get_headers method to provide tokenless auth scope headers if token is not provided in the session. :param session: The session object that the auth_plugin belongs to. :type session: keystoneauth1.session.Session :returns: Headers that are set to authenticate a message or None for failure. Note that when checking this value that the empty dict is a valid, non-failure response. :rtype: dict """ scope_headers = {} if self.project_id: scope_headers['X-Project-Id'] = self.project_id elif self.project_name: scope_headers['X-Project-Name'] = self.project_name if self.project_domain_id: scope_headers['X-Project-Domain-Id'] = ( self.project_domain_id) elif self.project_domain_name: scope_headers['X-Project-Domain-Name'] = ( self.project_domain_name) else: LOG.warning( 'Neither Project Domain ID nor Project Domain Name was ' 'provided.') return None elif self.domain_id: scope_headers['X-Domain-Id'] = self.domain_id elif self.domain_name: scope_headers['X-Domain-Name'] = self.domain_name else: LOG.warning( 'Neither Project nor Domain scope was provided.') return None return scope_headers def get_endpoint(self, session, service_type=None, **kwargs): """Return a valid endpoint for a service. :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :param string service_type: The type of service to lookup the endpoint for. This plugin will return None (failure) if service_type is not provided. :return: A valid endpoint URL or None if not available. :rtype: string or None """ if (service_type is plugin.AUTH_INTERFACE or service_type.lower() == 'identity'): return self.auth_url return None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/identity/v3/totp.py0000664000175000017500000000624700000000000022704 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy from keystoneauth1.identity.v3 import base __all__ = ('TOTPMethod', 'TOTP') class TOTPMethod(base.AuthMethod): """Construct a User/Passcode based authentication method. :param string passcode: TOTP passcode for authentication. :param string username: Username for authentication. :param string user_id: User ID for authentication. :param string user_domain_id: User's domain ID for authentication. :param string user_domain_name: User's domain name for authentication. """ _method_parameters = ['user_id', 'username', 'user_domain_id', 'user_domain_name', 'passcode'] def get_auth_data(self, session, auth, headers, **kwargs): user = {'passcode': self.passcode} if self.user_id: user['id'] = self.user_id elif self.username: user['name'] = self.username if self.user_domain_id: user['domain'] = {'id': self.user_domain_id} elif self.user_domain_name: user['domain'] = {'name': self.user_domain_name} return 'totp', {'user': user} def get_cache_id_elements(self): # NOTE(gyee): passcode is not static so we cannot use it as part of # the key in caching. params = copy.copy(self._method_parameters) params.remove('passcode') return dict(('totp_%s' % p, getattr(self, p)) for p in self._method_parameters) class TOTP(base.AuthConstructor): """A plugin for authenticating with a username and TOTP passcode. :param string auth_url: Identity service endpoint for authentication. :param string passcode: TOTP passcode for authentication. :param string username: Username for authentication. :param string user_id: User ID for authentication. :param string user_domain_id: User's domain ID for authentication. :param string user_domain_name: User's domain name for authentication. :param string trust_id: Trust ID for trust scoping. :param string domain_id: Domain ID for domain scoping. :param string domain_name: Domain name for domain scoping. :param string project_id: Project ID for project scoping. :param string project_name: Project name for project scoping. :param string project_domain_id: Project's domain ID for project. :param string project_domain_name: Project's domain name for project. :param bool reauthenticate: Allow fetching a new token if the current one is going to expire. (optional) default True """ _auth_method_class = TOTPMethod ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2967944 keystoneauth1-4.4.0/keystoneauth1/loading/0000775000175000017500000000000000000000000020567 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/__init__.py0000664000175000017500000000561600000000000022710 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # flake8: noqa: F405 from keystoneauth1.loading import adapter from keystoneauth1.loading.base import * # noqa from keystoneauth1.loading import cli from keystoneauth1.loading import conf from keystoneauth1.loading.identity import * # noqa from keystoneauth1.loading.opts import * # noqa from keystoneauth1.loading import session register_auth_argparse_arguments = cli.register_argparse_arguments load_auth_from_argparse_arguments = cli.load_from_argparse_arguments get_auth_common_conf_options = conf.get_common_conf_options get_auth_plugin_conf_options = conf.get_plugin_conf_options register_auth_conf_options = conf.register_conf_options load_auth_from_conf_options = conf.load_from_conf_options register_session_argparse_arguments = session.register_argparse_arguments load_session_from_argparse_arguments = session.load_from_argparse_arguments register_session_conf_options = session.register_conf_options load_session_from_conf_options = session.load_from_conf_options get_session_conf_options = session.get_conf_options register_adapter_argparse_arguments = adapter.register_argparse_arguments register_service_adapter_argparse_arguments = ( adapter.register_service_argparse_arguments) register_adapter_conf_options = adapter.register_conf_options load_adapter_from_conf_options = adapter.load_from_conf_options get_adapter_conf_options = adapter.get_conf_options __all__ = ( # loading.base 'BaseLoader', 'get_available_plugin_names', 'get_available_plugin_loaders', 'get_plugin_loader', 'PLUGIN_NAMESPACE', # loading.identity 'BaseIdentityLoader', 'BaseV2Loader', 'BaseV3Loader', 'BaseFederationLoader', 'BaseGenericLoader', # auth cli 'register_auth_argparse_arguments', 'load_auth_from_argparse_arguments', # auth conf 'get_auth_common_conf_options', 'get_auth_plugin_conf_options', 'register_auth_conf_options', 'load_auth_from_conf_options', # session 'register_session_argparse_arguments', 'load_session_from_argparse_arguments', 'register_session_conf_options', 'load_session_from_conf_options', 'get_session_conf_options', # adapter 'register_adapter_argparse_arguments', 'register_service_adapter_argparse_arguments', 'register_adapter_conf_options', 'load_adapter_from_conf_options', 'get_adapter_conf_options', # loading.opts 'Opt', ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2967944 keystoneauth1-4.4.0/keystoneauth1/loading/_plugins/0000775000175000017500000000000000000000000022407 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/_plugins/__init__.py0000664000175000017500000000000000000000000024506 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/_plugins/admin_token.py0000664000175000017500000000334600000000000025257 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import loading from keystoneauth1 import token_endpoint class AdminToken(loading.BaseLoader): """Use an existing token and a known endpoint to perform requests. This plugin is primarily useful for development or for use with identity service ADMIN tokens. Because this token is used directly there is no fetching a service catalog or determining scope information and so it cannot be used by clients that expect use this scope information. Because there is no service catalog the endpoint that is supplied with initialization is used for all operations performed with this plugin so must be the full base URL to an actual service. """ @property def plugin_class(self): return token_endpoint.Token def get_options(self): options = super(AdminToken, self).get_options() options.extend([ loading.Opt('endpoint', deprecated=[loading.Opt('url')], help='The endpoint that will always be used'), loading.Opt('token', secret=True, help='The token that will always be used'), ]) return options ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/_plugins/http_basic.py0000664000175000017500000000313400000000000025102 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import http_basic from keystoneauth1 import loading class HTTPBasicAuth(loading.BaseLoader): """Use HTTP Basic authentication to perform requests. This can be used to instantiate clients for services deployed in standalone mode. There is no fetching a service catalog or determining scope information and so it cannot be used by clients that expect to use this scope information. """ @property def plugin_class(self): return http_basic.HTTPBasicAuth def get_options(self): options = super(HTTPBasicAuth, self).get_options() options.extend([ loading.Opt('username', help='Username', deprecated=[loading.Opt('user-name')]), loading.Opt('password', secret=True, prompt='Password: ', help="User's password"), loading.Opt('endpoint', help='The endpoint that will always be used'), ]) return options ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2967944 keystoneauth1-4.4.0/keystoneauth1/loading/_plugins/identity/0000775000175000017500000000000000000000000024240 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/_plugins/identity/__init__.py0000664000175000017500000000000000000000000026337 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/_plugins/identity/generic.py0000664000175000017500000000525000000000000026230 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import identity from keystoneauth1 import loading class Token(loading.BaseGenericLoader): """Given an existing token rescope it to another target. This plugin uses the Identity service's rescope mechanism to get a new token based upon an existing token. Because an auth plugin requires a service catalog and scope information it is often easier to fetch a new token based on an existing one than validate and reuse the one you already have. As a generic plugin this plugin is identity version independent and will discover available versions before use. This means it expects to be providen an unversioned URL to operate against. """ @property def plugin_class(self): return identity.Token def get_options(self): options = super(Token, self).get_options() options.extend([ loading.Opt('token', secret=True, help='Token to authenticate with'), ]) return options class Password(loading.BaseGenericLoader): """Authenticate via a username and password. Authenticate to the identity service using an inbuilt username and password. This is the standard and most common form of authentication. As a generic plugin this plugin is identity version independent and will discover available versions before use. This means it expects to be providen an unversioned URL to operate against. """ @property def plugin_class(self): return identity.Password def get_options(cls): options = super(Password, cls).get_options() options.extend([ loading.Opt('user-id', help='User id'), loading.Opt('username', help='Username', deprecated=[loading.Opt('user-name')]), loading.Opt('user-domain-id', help="User's domain id"), loading.Opt('user-domain-name', help="User's domain name"), loading.Opt('password', secret=True, prompt='Password: ', help="User's password"), ]) return options ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/_plugins/identity/v2.py0000664000175000017500000000304700000000000025145 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import identity from keystoneauth1 import loading class Token(loading.BaseV2Loader): @property def plugin_class(self): return identity.V2Token def get_options(self): options = super(Token, self).get_options() options.extend([ loading.Opt('token', secret=True, help='Token'), ]) return options class Password(loading.BaseV2Loader): @property def plugin_class(self): return identity.V2Password def get_options(self): options = super(Password, self).get_options() options.extend([ loading.Opt('username', deprecated=[loading.Opt('user-name')], help='Username to login with'), loading.Opt('user-id', help='User ID to login with'), loading.Opt('password', secret=True, prompt='Password: ', help='Password to use'), ]) return options ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/_plugins/identity/v3.py0000664000175000017500000002657300000000000025157 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import exceptions from keystoneauth1 import identity from keystoneauth1 import loading def _add_common_identity_options(options): options.extend([ loading.Opt('user-id', help='User ID'), loading.Opt('username', help='Username', deprecated=[loading.Opt('user-name')]), loading.Opt('user-domain-id', help="User's domain id"), loading.Opt('user-domain-name', help="User's domain name"), ]) def _assert_identity_options(options): if (options.get('username') and not (options.get('user_domain_name') or options.get('user_domain_id'))): m = "You have provided a username. In the V3 identity API a " \ "username is only unique within a domain so you must " \ "also provide either a user_domain_id or user_domain_name." raise exceptions.OptionError(m) class Password(loading.BaseV3Loader): @property def plugin_class(self): return identity.V3Password def get_options(self): options = super(Password, self).get_options() _add_common_identity_options(options) options.extend([ loading.Opt('password', secret=True, prompt='Password: ', help="User's password"), ]) return options def load_from_options(self, **kwargs): _assert_identity_options(kwargs) return super(Password, self).load_from_options(**kwargs) class Token(loading.BaseV3Loader): @property def plugin_class(self): return identity.V3Token def get_options(self): options = super(Token, self).get_options() options.extend([ loading.Opt('token', secret=True, help='Token to authenticate with'), ]) return options class _OpenIDConnectBase(loading.BaseFederationLoader): def load_from_options(self, **kwargs): if not (kwargs.get('access_token_endpoint') or kwargs.get('discovery_endpoint')): m = ("You have to specify either an 'access-token-endpoint' or " "a 'discovery-endpoint'.") raise exceptions.OptionError(m) return super(_OpenIDConnectBase, self).load_from_options(**kwargs) def get_options(self): options = super(_OpenIDConnectBase, self).get_options() options.extend([ loading.Opt('client-id', help='OAuth 2.0 Client ID'), loading.Opt('client-secret', secret=True, help='OAuth 2.0 Client Secret'), loading.Opt('openid-scope', default="openid profile", dest="scope", help='OpenID Connect scope that is requested from ' 'authorization server. Note that the OpenID ' 'Connect specification states that "openid" ' 'must be always specified.'), loading.Opt('access-token-endpoint', help='OpenID Connect Provider Token Endpoint. Note ' 'that if a discovery document is being passed this ' 'option will override the endpoint provided by the ' 'server in the discovery document.'), loading.Opt('discovery-endpoint', help='OpenID Connect Discovery Document URL. ' 'The discovery document will be used to obtain the ' 'values of the access token endpoint and the ' 'authentication endpoint. This URL should look like ' 'https://idp.example.org/.well-known/' 'openid-configuration'), loading.Opt('access-token-type', help='OAuth 2.0 Authorization Server Introspection ' 'token type, it is used to decide which type ' 'of token will be used when processing token ' 'introspection. Valid values are: ' '"access_token" or "id_token"'), ]) return options class OpenIDConnectClientCredentials(_OpenIDConnectBase): @property def plugin_class(self): return identity.V3OidcClientCredentials def get_options(self): options = super(OpenIDConnectClientCredentials, self).get_options() return options class OpenIDConnectPassword(_OpenIDConnectBase): @property def plugin_class(self): return identity.V3OidcPassword def get_options(self): options = super(OpenIDConnectPassword, self).get_options() options.extend([ loading.Opt('username', help='Username', required=True), loading.Opt('password', secret=True, help='Password', required=True), ]) return options class OpenIDConnectAuthorizationCode(_OpenIDConnectBase): @property def plugin_class(self): return identity.V3OidcAuthorizationCode def get_options(self): options = super(OpenIDConnectAuthorizationCode, self).get_options() options.extend([ loading.Opt('redirect-uri', help='OpenID Connect Redirect URL'), loading.Opt('code', secret=True, required=True, deprecated=[loading.Opt('authorization-code')], help='OAuth 2.0 Authorization Code'), ]) return options class OpenIDConnectAccessToken(loading.BaseFederationLoader): @property def plugin_class(self): return identity.V3OidcAccessToken def get_options(self): options = super(OpenIDConnectAccessToken, self).get_options() options.extend([ loading.Opt('access-token', secret=True, required=True, help='OAuth 2.0 Access Token'), ]) return options class TOTP(loading.BaseV3Loader): @property def plugin_class(self): return identity.V3TOTP def get_options(self): options = super(TOTP, self).get_options() _add_common_identity_options(options) options.extend([ loading.Opt( 'passcode', secret=True, prompt='TOTP passcode: ', help="User's TOTP passcode"), ]) return options def load_from_options(self, **kwargs): _assert_identity_options(kwargs) return super(TOTP, self).load_from_options(**kwargs) class TokenlessAuth(loading.BaseLoader): @property def plugin_class(self): return identity.V3TokenlessAuth def get_options(self): options = super(TokenlessAuth, self).get_options() options.extend([ loading.Opt('auth-url', required=True, help='Authentication URL'), loading.Opt('domain-id', help='Domain ID to scope to'), loading.Opt('domain-name', help='Domain name to scope to'), loading.Opt('project-id', help='Project ID to scope to'), loading.Opt('project-name', help='Project name to scope to'), loading.Opt('project-domain-id', help='Domain ID containing project'), loading.Opt('project-domain-name', help='Domain name containing project'), ]) return options def load_from_options(self, **kwargs): if (not kwargs.get('domain_id') and not kwargs.get('domain_name') and not kwargs.get('project_id') and not kwargs.get('project_name') or (kwargs.get('project_name') and not (kwargs.get('project_domain_name') or kwargs.get('project_domain_id')))): m = ('You need to provide either a domain_name, domain_id, ' 'project_id or project_name. ' 'If you have provided a project_name, in the V3 identity ' 'API a project_name is only unique within a domain so ' 'you must also provide either a project_domain_id or ' 'project_domain_name.') raise exceptions.OptionError(m) return super(TokenlessAuth, self).load_from_options(**kwargs) class ApplicationCredential(loading.BaseV3Loader): @property def plugin_class(self): return identity.V3ApplicationCredential def get_options(self): options = super(ApplicationCredential, self).get_options() _add_common_identity_options(options) options.extend([ loading.Opt('application_credential_secret', secret=True, required=True, help="Application credential auth secret"), ]), options.extend([ loading.Opt('application_credential_id', help='Application credential ID'), ]), options.extend([ loading.Opt('application_credential_name', help='Application credential name'), ]) return options def load_from_options(self, **kwargs): _assert_identity_options(kwargs) if (not kwargs.get('application_credential_id') and not kwargs.get('application_credential_name')): m = ('You must provide either an application credential ID or an ' 'application credential name and user.') raise exceptions.OptionError(m) if not kwargs.get('application_credential_secret'): m = ('You must provide an auth secret.') raise exceptions.OptionError(m) return super(ApplicationCredential, self).load_from_options(**kwargs) class MultiFactor(loading.BaseV3Loader): def __init__(self, *args, **kwargs): self._methods = None return super(MultiFactor, self).__init__(*args, **kwargs) @property def plugin_class(self): return identity.V3MultiFactor def get_options(self): options = super(MultiFactor, self).get_options() options.extend([ loading.Opt( 'auth_methods', required=True, help="Methods to authenticate with."), ]) if self._methods: options_dict = {o.name: o for o in options} for method in self._methods: method_opts = loading.get_plugin_options(method) for opt in method_opts: options_dict[opt.name] = opt options = list(options_dict.values()) return options def load_from_options(self, **kwargs): _assert_identity_options(kwargs) if 'auth_methods' not in kwargs: raise exceptions.OptionError("methods is a required option.") self._methods = kwargs['auth_methods'] return super(MultiFactor, self).load_from_options(**kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/_plugins/noauth.py0000664000175000017500000000240200000000000024255 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import loading from keystoneauth1 import noauth class NoAuth(loading.BaseLoader): """Use no tokens to perform requests. This can be used to instantiate clients for services deployed in noauth/standalone mode. There is no fetching a service catalog or determining scope information and so it cannot be used by clients that expect to use this scope information. """ @property def plugin_class(self): return noauth.NoAuth def get_options(self): options = super(NoAuth, self).get_options() options.extend([ loading.Opt('endpoint', help='The endpoint that will always be used'), ]) return options ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/_utils.py0000664000175000017500000000263000000000000022441 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. cfg = None _NOT_FOUND = object() def get_oslo_config(): """Runtime load the oslo.config object. In performance optimization of openstackclient it was determined that even optimistically loading oslo.config if available had a performance cost. Given that we used to only raise the ImportError when the function was called also attempt to do the import to do everything at runtime. """ global cfg # First Call if not cfg: try: from oslo_config import cfg except ImportError: cfg = _NOT_FOUND if cfg is _NOT_FOUND: raise ImportError("oslo.config is not an automatic dependency of " "keystoneauth. If you wish to use oslo.config " "you need to import it into your application's " "requirements file. ") return cfg ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/adapter.py0000664000175000017500000004001200000000000022556 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import adapter from keystoneauth1.loading import _utils from keystoneauth1.loading import base __all__ = ('register_argparse_arguments', 'register_service_argparse_arguments', 'register_conf_options', 'load_from_conf_options', 'get_conf_options') class Adapter(base.BaseLoader): @property def plugin_class(self): return adapter.Adapter def get_options(self): return [] @staticmethod def get_conf_options(include_deprecated=True, deprecated_opts=None): """Get oslo_config options that are needed for a :py:class:`.Adapter`. These may be useful without being registered for config file generation or to manipulate the options before registering them yourself. The options that are set are: :service_type: The default service_type for URL discovery. :service_name: The default service_name for URL discovery. :interface: The default interface for URL discovery. (deprecated) :valid_interfaces: List of acceptable interfaces for URL discovery. Can be a list of any of 'public', 'internal' or 'admin'. :region_name: The default region_name for URL discovery. :endpoint_override: Always use this endpoint URL for requests for this client. :version: The minimum version restricted to a given Major API. Mutually exclusive with min_version and max_version. :min_version: The minimum major version of a given API, intended to be used as the lower bound of a range with max_version. Mutually exclusive with version. If min_version is given with no max_version it is as if max version is 'latest'. :max_version: The maximum major version of a given API, intended to be used as the upper bound of a range with min_version. Mutually exclusive with version. :param include_deprecated: If True (the default, for backward compatibility), deprecated options are included in the result. If False, they are excluded. :param dict deprecated_opts: Deprecated options that should be included in the definition of new options. This should be a dict from the name of the new option to a list of oslo.DeprecatedOpts that correspond to the new option. (optional) For example, to support the ``api_endpoint`` option pointing to the new ``endpoint_override`` option name:: old_opt = oslo_cfg.DeprecatedOpt('api_endpoint', 'old_group') deprecated_opts={'endpoint_override': [old_opt]} :returns: A list of oslo_config options. """ cfg = _utils.get_oslo_config() if deprecated_opts is None: deprecated_opts = {} # This is goofy, but need to support hyphens *or* underscores deprecated_opts = {name.replace('_', '-'): opt for name, opt in deprecated_opts.items()} opts = [cfg.StrOpt('service-type', deprecated_opts=deprecated_opts.get('service-type'), help='The default service_type for endpoint URL ' 'discovery.'), cfg.StrOpt('service-name', deprecated_opts=deprecated_opts.get('service-name'), help='The default service_name for endpoint URL ' 'discovery.'), cfg.ListOpt('valid-interfaces', deprecated_opts=deprecated_opts.get( 'valid-interfaces'), help='List of interfaces, in order of preference, ' 'for endpoint URL.'), cfg.StrOpt('region-name', deprecated_opts=deprecated_opts.get('region-name'), help='The default region_name for endpoint URL ' 'discovery.'), cfg.StrOpt('endpoint-override', deprecated_opts=deprecated_opts.get( 'endpoint-override'), help='Always use this endpoint URL for requests ' 'for this client. NOTE: The unversioned ' 'endpoint should be specified here; to ' 'request a particular API version, use the ' '`version`, `min-version`, and/or ' '`max-version` options.'), cfg.StrOpt('version', deprecated_opts=deprecated_opts.get('version'), help='Minimum Major API version within a given ' 'Major API version for endpoint URL ' 'discovery. Mutually exclusive with ' 'min_version and max_version'), cfg.StrOpt('min-version', deprecated_opts=deprecated_opts.get('min-version'), help='The minimum major version of a given API, ' 'intended to be used as the lower bound of a ' 'range with max_version. Mutually exclusive ' 'with version. If min_version is given with ' 'no max_version it is as if max version is ' '"latest".'), cfg.StrOpt('max-version', deprecated_opts=deprecated_opts.get('max-version'), help='The maximum major version of a given API, ' 'intended to be used as the upper bound of a ' 'range with min_version. Mutually exclusive ' 'with version.'), cfg.IntOpt('connect-retries', deprecated_opts=deprecated_opts.get( 'connect-retries'), help='The maximum number of retries that should be ' 'attempted for connection errors.'), cfg.FloatOpt('connect-retry-delay', deprecated_opts=deprecated_opts.get( 'connect-retry-delay'), help='Delay (in seconds) between two retries ' 'for connection errors. If not set, ' 'exponential retry starting with 0.5 ' 'seconds up to a maximum of 60 seconds ' 'is used.'), cfg.IntOpt('status-code-retries', deprecated_opts=deprecated_opts.get( 'status-code-retries'), help='The maximum number of retries that should be ' 'attempted for retriable HTTP status codes.'), cfg.FloatOpt('status-code-retry-delay', deprecated_opts=deprecated_opts.get( 'status-code-retry-delay'), help='Delay (in seconds) between two retries ' 'for retriable status codes. If not set, ' 'exponential retry starting with 0.5 ' 'seconds up to a maximum of 60 seconds ' 'is used.'), ] if include_deprecated: opts += [ cfg.StrOpt('interface', help='The default interface for endpoint URL ' 'discovery.', deprecated_for_removal=True, deprecated_reason='Using valid-interfaces is' ' preferrable because it is' ' capable of accepting a list of' ' possible interfaces.'), ] return opts def register_conf_options(self, conf, group, include_deprecated=True, deprecated_opts=None): """Register the oslo_config options that are needed for an Adapter. The options that are set are: :service_type: The default service_type for URL discovery. :service_name: The default service_name for URL discovery. :interface: The default interface for URL discovery. (deprecated) :valid_interfaces: List of acceptable interfaces for URL discovery. Can be a list of any of 'public', 'internal' or 'admin'. :region_name: The default region_name for URL discovery. :endpoint_override: Always use this endpoint URL for requests for this client. :version: The minimum version restricted to a given Major API. Mutually exclusive with min_version and max_version. :min_version: The minimum major version of a given API, intended to be used as the lower bound of a range with max_version. Mutually exclusive with version. If min_version is given with no max_version it is as if max version is 'latest'. :max_version: The maximum major version of a given API, intended to be used as the upper bound of a range with min_version. Mutually exclusive with version. :connect_retries: The maximum number of retries that should be attempted for connection errors. :status_code_retries: The maximum number of retries that should be attempted for retriable HTTP status codes. :param oslo_config.Cfg conf: config object to register with. :param string group: The ini group to register options in. :param include_deprecated: If True (the default, for backward compatibility), deprecated options are registered. If False, they are excluded. :param dict deprecated_opts: Deprecated options that should be included in the definition of new options. This should be a dict from the name of the new option to a list of oslo.DeprecatedOpts that correspond to the new option. (optional) For example, to support the ``api_endpoint`` option pointing to the new ``endpoint_override`` option name:: old_opt = oslo_cfg.DeprecatedOpt('api_endpoint', 'old_group') deprecated_opts={'endpoint_override': [old_opt]} :returns: The list of options that was registered. """ opts = self.get_conf_options(include_deprecated=include_deprecated, deprecated_opts=deprecated_opts) conf.register_group(_utils.get_oslo_config().OptGroup(group)) conf.register_opts(opts, group=group) return opts def load_from_conf_options(self, conf, group, **kwargs): """Create an Adapter object from an oslo_config object. The options must have been previously registered with register_conf_options. :param oslo_config.Cfg conf: config object to register with. :param string group: The ini group to register options in. :param dict kwargs: Additional parameters to pass to Adapter construction. :returns: A new Adapter object. :rtype: :py:class:`.Adapter` """ c = conf[group] process_conf_options(c, kwargs) return self.load_from_options(**kwargs) def process_conf_options(confgrp, kwargs): """Set Adapter constructor kwargs based on conf options. :param oslo_config.cfg.GroupAttr confgrp: Config object group containing options to inspect. :param dict kwargs: Keyword arguments suitable for the constructor of keystoneauth1.adapter.Adapter. Will be modified by this method. Values already set remain unaffected. :raise TypeError: If invalid conf option values or combinations are found. """ if confgrp.valid_interfaces and getattr(confgrp, 'interface', None): raise TypeError("interface and valid_interfaces are mutually" " exclusive. Please use valid_interfaces.") if confgrp.valid_interfaces: for iface in confgrp.valid_interfaces: if iface not in ('public', 'internal', 'admin'): # TODO(efried): s/valies/values/ - are we allowed to fix this? raise TypeError("'{iface}' is not a valid value for" " valid_interfaces. Valid valies are" " public, internal or admin".format( iface=iface)) kwargs.setdefault('interface', confgrp.valid_interfaces) elif hasattr(confgrp, 'interface'): kwargs.setdefault('interface', confgrp.interface) kwargs.setdefault('service_type', confgrp.service_type) kwargs.setdefault('service_name', confgrp.service_name) kwargs.setdefault('region_name', confgrp.region_name) kwargs.setdefault('endpoint_override', confgrp.endpoint_override) kwargs.setdefault('version', confgrp.version) kwargs.setdefault('min_version', confgrp.min_version) kwargs.setdefault('max_version', confgrp.max_version) if kwargs['version'] and ( kwargs['max_version'] or kwargs['min_version']): raise TypeError( "version is mutually exclusive with min_version and" " max_version") kwargs.setdefault('connect_retries', confgrp.connect_retries) kwargs.setdefault('connect_retry_delay', confgrp.connect_retry_delay) kwargs.setdefault('status_code_retries', confgrp.status_code_retries) kwargs.setdefault('status_code_retry_delay', confgrp.status_code_retry_delay) def register_argparse_arguments(*args, **kwargs): return adapter.register_adapter_argparse_arguments(*args, **kwargs) def register_service_argparse_arguments(*args, **kwargs): return adapter.register_service_adapter_argparse_arguments(*args, **kwargs) def register_conf_options(*args, **kwargs): return Adapter().register_conf_options(*args, **kwargs) def load_from_conf_options(*args, **kwargs): return Adapter().load_from_conf_options(*args, **kwargs) def get_conf_options(*args, **kwargs): return Adapter.get_conf_options(*args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/base.py0000664000175000017500000001435100000000000022057 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc import six import stevedore from keystoneauth1 import exceptions PLUGIN_NAMESPACE = 'keystoneauth1.plugin' __all__ = ('get_available_plugin_names', 'get_available_plugin_loaders', 'get_plugin_loader', 'get_plugin_options', 'BaseLoader', 'PLUGIN_NAMESPACE') def _auth_plugin_available(ext): """Read the value of available for whether to load this plugin.""" return ext.obj.available def get_available_plugin_names(): """Get the names of all the plugins that are available on the system. This is particularly useful for help and error text to prompt a user for example what plugins they may specify. :returns: A list of names. :rtype: frozenset """ mgr = stevedore.EnabledExtensionManager(namespace=PLUGIN_NAMESPACE, check_func=_auth_plugin_available, invoke_on_load=True, propagate_map_exceptions=True) return frozenset(mgr.names()) def get_available_plugin_loaders(): """Retrieve all the plugin classes available on the system. :returns: A dict with plugin entrypoint name as the key and the plugin loader as the value. :rtype: dict """ mgr = stevedore.EnabledExtensionManager(namespace=PLUGIN_NAMESPACE, check_func=_auth_plugin_available, invoke_on_load=True, propagate_map_exceptions=True) return dict(mgr.map(lambda ext: (ext.entry_point.name, ext.obj))) def get_plugin_loader(name): """Retrieve a plugin class by its entrypoint name. :param str name: The name of the object to get. :returns: An auth plugin class. :rtype: :py:class:`keystoneauth1.loading.BaseLoader` :raises keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin: if a plugin cannot be created. """ try: mgr = stevedore.DriverManager(namespace=PLUGIN_NAMESPACE, invoke_on_load=True, name=name) except RuntimeError: raise exceptions.NoMatchingPlugin(name) return mgr.driver def get_plugin_options(name): """Get the options for a specific plugin. This will be the list of options that is registered and loaded by the specified plugin. :returns: A list of :py:class:`keystoneauth1.loading.Opt` options. :raises keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin: if a plugin cannot be created. """ return get_plugin_loader(name).get_options() @six.add_metaclass(abc.ABCMeta) class BaseLoader(object): @property def plugin_class(self): raise NotImplementedError() def create_plugin(self, **kwargs): """Create a plugin from the options available for the loader. Given the options that were specified by the loader create an appropriate plugin. You can override this function in your loader. This used to be specified by providing the plugin_class property and this is still supported, however specifying a property didn't let you choose a plugin type based upon the options that were presented. Override this function if you wish to return different plugins based on the options presented, otherwise you can simply provide the plugin_class property. Added 2.9 """ return self.plugin_class(**kwargs) @abc.abstractmethod def get_options(self): """Return the list of parameters associated with the auth plugin. This list may be used to generate CLI or config arguments. :returns: A list of Param objects describing available plugin parameters. :rtype: list """ return [] @property def available(self): """Return if the plugin is available for loading. If a plugin is missing dependencies or for some other reason should not be available to the current system it should override this property and return False to exclude itself from the plugin list. :rtype: bool """ return True def load_from_options(self, **kwargs): """Create a plugin from the arguments retrieved from get_options. A client can override this function to do argument validation or to handle differences between the registered options and what is required to create the plugin. """ missing_required = [o for o in self.get_options() if o.required and kwargs.get(o.dest) is None] if missing_required: raise exceptions.MissingRequiredOptions(missing_required) return self.create_plugin(**kwargs) def load_from_options_getter(self, getter, **kwargs): """Load a plugin from getter function that returns appropriate values. To handle cases other than the provided CONF and CLI loading you can specify a custom loader function that will be queried for the option value. The getter is a function that takes a :py:class:`keystoneauth1.loading.Opt` and returns a value to load with. :param getter: A function that returns a value for the given opt. :type getter: callable :returns: An authentication Plugin. :rtype: :py:class:`keystoneauth1.plugin.BaseAuthPlugin` """ for opt in (o for o in self.get_options() if o.dest not in kwargs): val = getter(opt) if val is not None: val = opt.type(val) kwargs[opt.dest] = val return self.load_from_options(**kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/cli.py0000664000175000017500000000725700000000000021723 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse import os from keystoneauth1.loading import base __all__ = ('register_argparse_arguments', 'load_from_argparse_arguments') def _register_plugin_argparse_arguments(parser, plugin): for opt in plugin.get_options(): parser.add_argument(*opt.argparse_args, default=opt.argparse_default, metavar=opt.metavar, help=opt.help, dest='os_%s' % opt.dest) def register_argparse_arguments(parser, argv, default=None): """Register CLI options needed to create a plugin. The function inspects the provided arguments so that it can also register the options required for that specific plugin if available. :param parser: the parser to attach argparse options to. :type parser: argparse.ArgumentParser :param list argv: the arguments provided to the appliation. :param str/class default: a default plugin name or a plugin object to use if one isn't specified by the CLI. default: None. :returns: The plugin class that will be loaded or None if not provided. :rtype: :class:`keystoneauth1.plugin.BaseAuthPlugin` :raises keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin: if a plugin cannot be created. """ in_parser = argparse.ArgumentParser(add_help=False) env_plugin = os.environ.get('OS_AUTH_TYPE', os.environ.get('OS_AUTH_PLUGIN', default)) for p in (in_parser, parser): p.add_argument('--os-auth-type', '--os-auth-plugin', metavar='', default=env_plugin, help='Authentication type to use') options, _args = in_parser.parse_known_args(argv) if not options.os_auth_type: return None if isinstance(options.os_auth_type, base.BaseLoader): msg = 'Default Authentication options' plugin = options.os_auth_type else: msg = 'Options specific to the %s plugin.' % options.os_auth_type plugin = base.get_plugin_loader(options.os_auth_type) group = parser.add_argument_group('Authentication Options', msg) _register_plugin_argparse_arguments(group, plugin) return plugin def load_from_argparse_arguments(namespace, **kwargs): """Retrieve the created plugin from the completed argparse results. Loads and creates the auth plugin from the information parsed from the command line by argparse. :param Namespace namespace: The result from CLI parsing. :returns: An auth plugin, or None if a name is not provided. :rtype: :class:`keystoneauth1.plugin.BaseAuthPlugin` :raises keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin: if a plugin cannot be created. """ if not namespace.os_auth_type: return None if isinstance(namespace.os_auth_type, type): plugin = namespace.os_auth_type else: plugin = base.get_plugin_loader(namespace.os_auth_type) def _getter(opt): return getattr(namespace, 'os_%s' % opt.dest) return plugin.load_from_options_getter(_getter, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/conf.py0000664000175000017500000001146600000000000022076 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.loading import base from keystoneauth1.loading import opts _AUTH_TYPE_OPT = opts.Opt('auth_type', deprecated=[opts.Opt('auth_plugin')], help='Authentication type to load') _section_help = 'Config Section from which to load plugin specific options' _AUTH_SECTION_OPT = opts.Opt('auth_section', help=_section_help) __all__ = ('get_common_conf_options', 'get_plugin_conf_options', 'register_conf_options', 'load_from_conf_options') def get_common_conf_options(): """Get the oslo_config options common for all auth plugins. These may be useful without being registered for config file generation or to manipulate the options before registering them yourself. The options that are set are: :auth_type: The name of the plugin to load. :auth_section: The config file section to load options from. :returns: A list of oslo_config options. """ return [_AUTH_TYPE_OPT._to_oslo_opt(), _AUTH_SECTION_OPT._to_oslo_opt()] def get_plugin_conf_options(plugin): """Get the oslo_config options for a specific plugin. This will be the list of config options that is registered and loaded by the specified plugin. :param plugin: The name of the plugin loader or a plugin loader object :type plugin: str or keystoneauth1._loading.BaseLoader :returns: A list of oslo_config options. """ try: getter = plugin.get_options except AttributeError: opts = base.get_plugin_options(plugin) else: opts = getter() return [o._to_oslo_opt() for o in opts] def register_conf_options(conf, group): """Register the oslo_config options that are needed for a plugin. This only registers the basic options shared by all plugins. Options that are specific to a plugin are loaded just before they are read. The defined options are: - auth_type: the name of the auth plugin that will be used for authentication. - auth_section: the group from which further auth plugin options should be taken. If section is not provided then the auth plugin options will be taken from the same group as provided in the parameters. :param conf: config object to register with. :type conf: oslo_config.cfg.ConfigOpts :param string group: The ini group to register options in. """ conf.register_opt(_AUTH_SECTION_OPT._to_oslo_opt(), group=group) # NOTE(jamielennox): plugins are allowed to specify a 'section' which is # the group that auth options should be taken from. If not present they # come from the same as the base options were registered in. If present # then the auth_plugin option may be read from that section so add that # option. if conf[group].auth_section: group = conf[group].auth_section conf.register_opt(_AUTH_TYPE_OPT._to_oslo_opt(), group=group) def load_from_conf_options(conf, group, **kwargs): """Load a plugin from an oslo_config CONF object. Each plugin will register their own required options and so there is no standard list and the plugin should be consulted. The base options should have been registered with register_conf_options before this function is called. :param conf: A conf object. :type conf: oslo_config.cfg.ConfigOpts :param str group: The group name that options should be read from. :returns: An authentication Plugin or None if a name is not provided :rtype: :class:`keystoneauth1.plugin.BaseAuthPlugin` :raises keystoneauth1.exceptions.auth_plugins.NoMatchingPlugin: if a plugin cannot be created. """ # NOTE(jamielennox): plugins are allowed to specify a 'section' which is # the group that auth options should be taken from. If not present they # come from the same as the base options were registered in. if conf[group].auth_section: group = conf[group].auth_section name = conf[group].auth_type if not name: return None plugin = base.get_plugin_loader(name) plugin_opts = plugin.get_options() oslo_opts = [o._to_oslo_opt() for o in plugin_opts] conf.register_opts(oslo_opts, group=group) def _getter(opt): return conf[group][opt.dest] return plugin.load_from_options_getter(_getter, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/identity.py0000664000175000017500000001423000000000000022772 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import exceptions from keystoneauth1.loading import base from keystoneauth1.loading import opts __all__ = ('BaseIdentityLoader', 'BaseV2Loader', 'BaseV3Loader', 'BaseFederationLoader', 'BaseGenericLoader') class BaseIdentityLoader(base.BaseLoader): """Base Option handling for identity plugins. This class defines options and handling that should be common across all plugins that are developed against the OpenStack identity service. It provides the options expected by the :py:class:`keystoneauth1.identity.BaseIdentityPlugin` class. """ def get_options(self): options = super(BaseIdentityLoader, self).get_options() options.extend([ opts.Opt('auth-url', required=True, help='Authentication URL'), ]) return options class BaseV2Loader(BaseIdentityLoader): """Base Option handling for identity plugins. This class defines options and handling that should be common to the V2 identity API. It provides the options expected by the :py:class:`keystoneauth1.identity.v2.Auth` class. """ def get_options(self): options = super(BaseV2Loader, self).get_options() options.extend([ opts.Opt('tenant-id', help='Tenant ID'), opts.Opt('tenant-name', help='Tenant Name'), opts.Opt('trust-id', help='Trust ID'), ]) return options class BaseV3Loader(BaseIdentityLoader): """Base Option handling for identity plugins. This class defines options and handling that should be common to the V3 identity API. It provides the options expected by the :py:class:`keystoneauth1.identity.v3.Auth` class. """ def get_options(self): options = super(BaseV3Loader, self).get_options() options.extend([ opts.Opt('system-scope', help='Scope for system operations'), opts.Opt('domain-id', help='Domain ID to scope to'), opts.Opt('domain-name', help='Domain name to scope to'), opts.Opt('project-id', help='Project ID to scope to'), opts.Opt('project-name', help='Project name to scope to'), opts.Opt('project-domain-id', help='Domain ID containing project'), opts.Opt('project-domain-name', help='Domain name containing project'), opts.Opt('trust-id', help='Trust ID'), ]) return options def load_from_options(self, **kwargs): if (kwargs.get('project_name') and not (kwargs.get('project_domain_name') or kwargs.get('project_domain_id'))): m = "You have provided a project_name. In the V3 identity API a " \ "project_name is only unique within a domain so you must " \ "also provide either a project_domain_id or " \ "project_domain_name." raise exceptions.OptionError(m) return super(BaseV3Loader, self).load_from_options(**kwargs) class BaseFederationLoader(BaseV3Loader): """Base Option handling for federation plugins. This class defines options and handling that should be common to the V3 identity federation API. It provides the options expected by the :py:class:`keystoneauth1.identity.v3.FederationBaseAuth` class. """ def get_options(self): options = super(BaseFederationLoader, self).get_options() options.extend([ opts.Opt('identity-provider', help="Identity Provider's name", required=True), opts.Opt('protocol', help='Protocol for federated plugin', required=True), ]) return options class BaseGenericLoader(BaseIdentityLoader): """Base Option handling for generic plugins. This class defines options and handling that should be common to generic plugins. These plugins target the OpenStack identity service however are designed to be independent of API version. It provides the options expected by the :py:class:`keystoneauth1.identity.v3.BaseGenericPlugin` class. """ def get_options(self): options = super(BaseGenericLoader, self).get_options() options.extend([ opts.Opt('system-scope', help='Scope for system operations'), opts.Opt('domain-id', help='Domain ID to scope to'), opts.Opt('domain-name', help='Domain name to scope to'), opts.Opt('project-id', help='Project ID to scope to', deprecated=[opts.Opt('tenant-id')]), opts.Opt('project-name', help='Project name to scope to', deprecated=[opts.Opt('tenant-name')]), opts.Opt('project-domain-id', help='Domain ID containing project'), opts.Opt('project-domain-name', help='Domain name containing project'), opts.Opt('trust-id', help='Trust ID'), opts.Opt('default-domain-id', help='Optional domain ID to use with v3 and v2 ' 'parameters. It will be used for both the user ' 'and project domain in v3 and ignored in ' 'v2 authentication.'), opts.Opt('default-domain-name', help='Optional domain name to use with v3 API and v2 ' 'parameters. It will be used for both the user ' 'and project domain in v3 and ignored in ' 'v2 authentication.'), ]) return options ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/opts.py0000664000175000017500000001275400000000000022137 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import itertools import os from keystoneauth1.loading import _utils __all__ = ('Opt',) class Opt(object): """An option required by an authentication plugin. Opts provide a means for authentication plugins that are going to be dynamically loaded to specify the parameters that are to be passed to the plugin on initialization. The Opt specifies information about the way the plugin parameter is to be represented in different loading mechanisms. When defining an Opt with a - the - should be present in the name parameter. This will automatically be converted to an _ when passing to the plugin initialization. For example, you should specify:: Opt('user-domain-id') which will pass the value as `user_domain_id` to the plugin's initialization. :param str name: The name of the option. :param callable type: The type of the option. This is a callable which is passed the raw option that was loaded (often a string) and is required to return the parameter in the type expected by __init__. :param str help: The help text that is shown along with the option. :param bool secret: If the parameter is secret it should not be printed or logged in debug output. :param str dest: the name of the argument that will be passed to __init__. This allows you to have a different name in loading than is used by the __init__ function. Defaults to the value of name. :param keystoneauth1.loading.Opt: A list of other options that are deprecated in favour of this one. This ensures the old options are still registered. :type opt: list(Opt) :param default: A default value that can be used if one is not provided. :param str metavar: The that should be printed in CLI help text. :param bool required: If the option is required to load the plugin. If a required option is not present loading should fail. :param str prompt: If the option can be requested via a prompt (where appropriate) set the string that should be used to prompt with. """ def __init__(self, name, type=str, help=None, secret=False, dest=None, deprecated=None, default=None, metavar=None, required=False, prompt=None): if not callable(type): raise TypeError('type must be callable') if dest is None: dest = name.replace('-', '_') self.name = name self.type = type self.help = help self.secret = secret self.required = required self.dest = dest self.deprecated = [] if deprecated is None else deprecated self.default = default self.metavar = metavar self.prompt = prompt # These are for oslo.config compat self.deprecated_opts = self.deprecated self.deprecated_for_removal = [] self.sample_default = None self.group = None def __repr__(self): """Return string representation of option name.""" return '' % self.name def _to_oslo_opt(self): cfg = _utils.get_oslo_config() deprecated_opts = [cfg.DeprecatedOpt(o.name) for o in self.deprecated] return cfg.Opt(name=self.name, type=self.type, help=self.help, secret=self.secret, required=self.required, dest=self.dest, deprecated_opts=deprecated_opts, metavar=self.metavar) def __eq__(self, other): """Define equality operator on option parameters.""" return (type(self) == type(other) and self.name == other.name and self.type == other.type and self.help == other.help and self.secret == other.secret and self.required == other.required and self.dest == other.dest and self.deprecated == other.deprecated and self.default == other.default and self.metavar == other.metavar) # NOTE: This function is only needed by Python 2. If we get to point where # we don't support Python 2 anymore, this function should be removed. def __ne__(self, other): """Define inequality operator on option parameters.""" return not self.__eq__(other) @property def _all_opts(self): return itertools.chain([self], self.deprecated) @property def argparse_args(self): return ['--os-%s' % o.name for o in self._all_opts] @property def argparse_default(self): # select the first ENV that is not false-y or return None for o in self._all_opts: v = os.environ.get('OS_%s' % o.name.replace('-', '_').upper()) if v: return v return self.default ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/loading/session.py0000664000175000017500000002521200000000000022626 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse import os from keystoneauth1.loading import _utils from keystoneauth1.loading import base from keystoneauth1 import session __all__ = ('register_argparse_arguments', 'load_from_argparse_arguments', 'register_conf_options', 'load_from_conf_options', 'get_conf_options') def _positive_non_zero_float(argument_value): if argument_value is None: return None try: value = float(argument_value) except ValueError: msg = "%s must be a float" % argument_value raise argparse.ArgumentTypeError(msg) if value <= 0: msg = "%s must be greater than 0" % argument_value raise argparse.ArgumentTypeError(msg) return value class Session(base.BaseLoader): @property def plugin_class(self): return session.Session def get_options(self): return [] def load_from_options(self, insecure=False, verify=None, cacert=None, cert=None, key=None, **kwargs): """Create a session with individual certificate parameters. Some parameters used to create a session don't lend themselves to be loaded from config/CLI etc. Create a session by converting those parameters into session __init__ parameters. """ if verify is None: if insecure: verify = False else: verify = cacert or True if cert and key: # passing cert and key together is deprecated in favour of the # requests lib form of having the cert and key as a tuple cert = (cert, key) return super(Session, self).load_from_options(verify=verify, cert=cert, **kwargs) def register_argparse_arguments(self, parser): session_group = parser.add_argument_group( 'API Connection Options', 'Options controlling the HTTP API Connections') session_group.add_argument( '--insecure', default=False, action='store_true', help='Explicitly allow client to perform ' '"insecure" TLS (https) requests. The ' 'server\'s certificate will not be verified ' 'against any certificate authorities. This ' 'option should be used with caution.') session_group.add_argument( '--os-cacert', metavar='', default=os.environ.get('OS_CACERT'), help='Specify a CA bundle file to use in ' 'verifying a TLS (https) server certificate. ' 'Defaults to env[OS_CACERT].') session_group.add_argument( '--os-cert', metavar='', default=os.environ.get('OS_CERT'), help='Defaults to env[OS_CERT].') session_group.add_argument( '--os-key', metavar='', default=os.environ.get('OS_KEY'), help='Defaults to env[OS_KEY].') session_group.add_argument( '--timeout', default=600, type=_positive_non_zero_float, metavar='', help='Set request timeout (in seconds).') session_group.add_argument( '--collect-timing', default=False, action='store_true', help='Collect per-API call timing information.') def load_from_argparse_arguments(self, namespace, **kwargs): kwargs.setdefault('insecure', namespace.insecure) kwargs.setdefault('cacert', namespace.os_cacert) kwargs.setdefault('cert', namespace.os_cert) kwargs.setdefault('key', namespace.os_key) kwargs.setdefault('timeout', namespace.timeout) kwargs.setdefault('collect_timing', namespace.collect_timing) return self.load_from_options(**kwargs) def get_conf_options(self, deprecated_opts=None): """Get oslo_config options that are needed for a :py:class:`.Session`. These may be useful without being registered for config file generation or to manipulate the options before registering them yourself. The options that are set are: :cafile: The certificate authority filename. :certfile: The client certificate file to present. :keyfile: The key for the client certificate. :insecure: Whether to ignore SSL verification. :timeout: The max time to wait for HTTP connections. :collect-timing: Whether to collect API timing information. :split-loggers: Whether to log requests to multiple loggers. :param dict deprecated_opts: Deprecated options that should be included in the definition of new options. This should be a dict from the name of the new option to a list of oslo.DeprecatedOpts that correspond to the new option. (optional) For example, to support the ``ca_file`` option pointing to the new ``cafile`` option name:: old_opt = oslo_cfg.DeprecatedOpt('ca_file', 'old_group') deprecated_opts={'cafile': [old_opt]} :returns: A list of oslo_config options. """ cfg = _utils.get_oslo_config() if deprecated_opts is None: deprecated_opts = {} return [cfg.StrOpt('cafile', deprecated_opts=deprecated_opts.get('cafile'), help='PEM encoded Certificate Authority to use ' 'when verifying HTTPs connections.'), cfg.StrOpt('certfile', deprecated_opts=deprecated_opts.get('certfile'), help='PEM encoded client certificate cert file'), cfg.StrOpt('keyfile', deprecated_opts=deprecated_opts.get('keyfile'), help='PEM encoded client certificate key file'), cfg.BoolOpt('insecure', default=False, deprecated_opts=deprecated_opts.get('insecure'), help='Verify HTTPS connections.'), cfg.IntOpt('timeout', deprecated_opts=deprecated_opts.get('timeout'), help='Timeout value for http requests'), cfg.BoolOpt('collect-timing', deprecated_opts=deprecated_opts.get( 'collect-timing'), default=False, help='Collect per-API call timing information.'), cfg.BoolOpt('split-loggers', deprecated_opts=deprecated_opts.get( 'split-loggers'), default=False, help='Log requests to multiple loggers.') ] def register_conf_options(self, conf, group, deprecated_opts=None): """Register the oslo_config options that are needed for a session. The options that are set are: :cafile: The certificate authority filename. :certfile: The client certificate file to present. :keyfile: The key for the client certificate. :insecure: Whether to ignore SSL verification. :timeout: The max time to wait for HTTP connections. :collect-timing: Whether to collect API timing information. :split-loggers: Whether to log requests to multiple loggers. :param oslo_config.Cfg conf: config object to register with. :param string group: The ini group to register options in. :param dict deprecated_opts: Deprecated options that should be included in the definition of new options. This should be a dict from the name of the new option to a list of oslo.DeprecatedOpts that correspond to the new option. (optional) For example, to support the ``ca_file`` option pointing to the new ``cafile`` option name:: old_opt = oslo_cfg.DeprecatedOpt('ca_file', 'old_group') deprecated_opts={'cafile': [old_opt]} :returns: The list of options that was registered. """ opts = self.get_conf_options(deprecated_opts=deprecated_opts) conf.register_group(_utils.get_oslo_config().OptGroup(group)) conf.register_opts(opts, group=group) return opts def load_from_conf_options(self, conf, group, **kwargs): """Create a session object from an oslo_config object. The options must have been previously registered with register_conf_options. :param oslo_config.Cfg conf: config object to register with. :param string group: The ini group to register options in. :param dict kwargs: Additional parameters to pass to session construction. :returns: A new session object. :rtype: :py:class:`.Session` """ c = conf[group] kwargs.setdefault('insecure', c.insecure) kwargs.setdefault('cacert', c.cafile) kwargs.setdefault('cert', c.certfile) kwargs.setdefault('key', c.keyfile) kwargs.setdefault('timeout', c.timeout) kwargs.setdefault('collect_timing', c.collect_timing) kwargs.setdefault('split_loggers', c.split_loggers) return self.load_from_options(**kwargs) def register_argparse_arguments(*args, **kwargs): return Session().register_argparse_arguments(*args, **kwargs) def load_from_argparse_arguments(*args, **kwargs): return Session().load_from_argparse_arguments(*args, **kwargs) def register_conf_options(*args, **kwargs): return Session().register_conf_options(*args, **kwargs) def load_from_conf_options(*args, **kwargs): return Session().load_from_conf_options(*args, **kwargs) def get_conf_options(*args, **kwargs): return Session().get_conf_options(*args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/noauth.py0000664000175000017500000000154500000000000021027 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import plugin class NoAuth(plugin.FixedEndpointPlugin): """A provider that will always use no auth. This is useful to unify session/adapter loading for services that might be deployed in standalone/noauth mode. """ def get_token(self, session, **kwargs): return 'notused' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/plugin.py0000664000175000017500000003535400000000000021034 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import discover # NOTE(jamielennox): The AUTH_INTERFACE is a special value that can be # requested from get_endpoint. If a plugin receives this as the value of # 'interface' it should return the initial URL that was passed to the plugin. AUTH_INTERFACE = object() IDENTITY_AUTH_HEADER_NAME = 'X-Auth-Token' class BaseAuthPlugin(object): """The basic structure of an authentication plugin. .. note:: See :doc:`/authentication-plugins` for a description of plugins provided by this library. """ def __init__(self): self._discovery_cache = {} def get_token(self, session, **kwargs): """Obtain a token. How the token is obtained is up to the plugin. If it is still valid it may be re-used, retrieved from cache or invoke an authentication request against a server. There are no required kwargs. They are passed directly to the auth plugin and they are implementation specific. Returning None will indicate that no token was able to be retrieved. This function is misplaced as it should only be required for auth plugins that use the 'X-Auth-Token' header. However due to the way plugins evolved this method is required and often called to trigger an authentication request on a new plugin. When implementing a new plugin it is advised that you implement this method, however if you don't require the 'X-Auth-Token' header override the `get_headers` method instead. :param session: A session object so the plugin can make HTTP calls. :type session: keystoneauth1.session.Session :return: A token to use. :rtype: string """ return None def get_auth_ref(self, session, **kwargs): """Return the authentication reference of an auth plugin. There are no required kwargs. They are passed directly to the auth plugin and they are implementation specific. :param session: A session object to be used for communication :type session: keystoneauth1.session.session """ return None def get_headers(self, session, **kwargs): """Fetch authentication headers for message. This is a more generalized replacement of the older get_token to allow plugins to specify different or additional authentication headers to the OpenStack standard 'X-Auth-Token' header. How the authentication headers are obtained is up to the plugin. If the headers are still valid they may be re-used, retrieved from cache or the plugin may invoke an authentication request against a server. The default implementation of get_headers calls the `get_token` method to enable older style plugins to continue functioning unchanged. Subclasses should feel free to completely override this function to provide the headers that they want. There are no required kwargs. They are passed directly to the auth plugin and they are implementation specific. Returning None will indicate that no token was able to be retrieved and that authorization was a failure. Adding no authentication data can be achieved by returning an empty dictionary. :param session: The session object that the auth_plugin belongs to. :type session: keystoneauth1.session.Session :returns: Headers that are set to authenticate a message or None for failure. Note that when checking this value that the empty dict is a valid, non-failure response. :rtype: dict """ token = self.get_token(session) if not token: return None return {IDENTITY_AUTH_HEADER_NAME: token} def get_endpoint_data(self, session, endpoint_override=None, discover_versions=True, **kwargs): """Return a valid endpoint data for a the service. :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :param str endpoint_override: URL to use for version discovery. :param bool discover_versions: Whether to get version metadata from the version discovery document even if it major api version info can be inferred from the url. (optional, defaults to True) :param kwargs: Ignored. :raises keystoneauth1.exceptions.http.HttpError: An error from an invalid HTTP response. :return: Valid EndpointData or None if not available. :rtype: `keystoneauth1.discover.EndpointData` or None """ if not endpoint_override: return None endpoint_data = discover.EndpointData(catalog_url=endpoint_override) if endpoint_data.api_version and not discover_versions: return endpoint_data return endpoint_data.get_versioned_data( session, cache=self._discovery_cache, discover_versions=discover_versions) def get_api_major_version(self, session, endpoint_override=None, **kwargs): """Get the major API version from the endpoint. :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :param str endpoint_override: URL to use for version discovery. :param kwargs: Ignored. :raises keystoneauth1.exceptions.http.HttpError: An error from an invalid HTTP response. :return: Valid EndpointData or None if not available. :rtype: `keystoneauth1.discover.EndpointData` or None """ endpoint_data = self.get_endpoint_data( session, endpoint_override=endpoint_override, discover_versions=False, **kwargs) if endpoint_data is None: return if endpoint_data.api_version is None: # No version detected from the URL, trying full discovery. endpoint_data = self.get_endpoint_data( session, endpoint_override=endpoint_override, discover_versions=True, **kwargs) if endpoint_data and endpoint_data.api_version: return endpoint_data.api_version return None def get_endpoint(self, session, **kwargs): """Return an endpoint for the client. There are no required keyword arguments to ``get_endpoint`` as a plugin implementation should use best effort with the information available to determine the endpoint. However there are certain standard options that will be generated by the clients and should be used by plugins: - ``service_type``: what sort of service is required. - ``service_name``: the name of the service in the catalog. - ``interface``: what visibility the endpoint should have. - ``region_name``: the region the endpoint exists in. :param session: The session object that the auth_plugin belongs to. :type session: keystoneauth1.session.Session :returns: The base URL that will be used to talk to the required service or None if not available. :rtype: string """ endpoint_data = self.get_endpoint_data( session, discover_versions=False, **kwargs) if not endpoint_data: return None return endpoint_data.url def get_connection_params(self, session, **kwargs): """Return any additional connection parameters required for the plugin. :param session: The session object that the auth_plugin belongs to. :type session: keystoneauth1.session.Session :returns: Headers that are set to authenticate a message or None for failure. Note that when checking this value that the empty dict is a valid, non-failure response. :rtype: dict """ return {} def invalidate(self): """Invalidate the current authentication data. This should result in fetching a new token on next call. A plugin may be invalidated if an Unauthorized HTTP response is returned to indicate that the token may have been revoked or is otherwise now invalid. :returns: True if there was something that the plugin did to invalidate. This means that it makes sense to try again. If nothing happens returns False to indicate give up. :rtype: bool """ return False def get_user_id(self, session, **kwargs): """Return a unique user identifier of the plugin. Wherever possible the user id should be inferred from the token however there are certain URLs and other places that require access to the currently authenticated user id. :param session: A session object so the plugin can make HTTP calls. :type session: keystoneauth1.session.Session :returns: A user identifier or None if one is not available. :rtype: str """ return None def get_project_id(self, session, **kwargs): """Return the project id that we are authenticated to. Wherever possible the project id should be inferred from the token however there are certain URLs and other places that require access to the currently authenticated project id. :param session: A session object so the plugin can make HTTP calls. :type session: keystoneauth1.session.Session :returns: A project identifier or None if one is not available. :rtype: str """ return None def get_sp_auth_url(self, session, sp_id, **kwargs): """Return auth_url from the Service Provider object. This url is used for obtaining unscoped federated token from remote cloud. :param sp_id: ID of the Service Provider to be queried. :type sp_id: string :returns: A Service Provider auth_url or None if one is not available. :rtype: str """ return None def get_sp_url(self, session, sp_id, **kwargs): """Return sp_url from the Service Provider object. This url is used for passing SAML2 assertion to the remote cloud. :param sp_id: ID of the Service Provider to be queried. :type sp_id: str :returns: A Service Provider sp_url or None if one is not available. :rtype: str """ return None def get_cache_id(self): """Fetch an identifier that uniquely identifies the auth options. The returned identifier need not be decomposable or otherwise provide anyway to recreate the plugin. It should not contain sensitive data in plaintext. This string MUST change if any of the parameters that are used to uniquely identity this plugin change. If get_cache_id returns a str value suggesting that caching is supported then get_auth_cache and set_auth_cache must also be implemented. :returns: A unique string for the set of options :rtype: str or None if this is unsupported or unavailable. """ return None def get_auth_state(self): """Retrieve the current authentication state for the plugin. Retrieve any internal state that represents the authenticated plugin. This should not fetch any new data if it is not present. :raises NotImplementedError: if the plugin does not support this feature. :returns: raw python data (which can be JSON serialized) that can be moved into another plugin (of the same type) to have the same authenticated state. :rtype: object or None if unauthenticated. """ raise NotImplementedError() def set_auth_state(self, data): """Install existing authentication state for a plugin. Take the output of get_auth_state and install that authentication state into the current authentication plugin. :raises NotImplementedError: if the plugin does not support this feature. """ raise NotImplementedError() class FixedEndpointPlugin(BaseAuthPlugin): """A base class for plugins that have one fixed endpoint.""" def __init__(self, endpoint=None): super(FixedEndpointPlugin, self).__init__() self.endpoint = endpoint def get_endpoint(self, session, **kwargs): """Return the supplied endpoint. Using this plugin the same endpoint is returned regardless of the parameters passed to the plugin. endpoint_override overrides the endpoint specified when constructing the plugin. """ return kwargs.get('endpoint_override') or self.endpoint def get_endpoint_data(self, session, endpoint_override=None, discover_versions=True, **kwargs): """Return a valid endpoint data for a the service. :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :param str endpoint_override: URL to use for version discovery. :param bool discover_versions: Whether to get version metadata from the version discovery document even if it major api version info can be inferred from the url. (optional, defaults to True) :param kwargs: Ignored. :raises keystoneauth1.exceptions.http.HttpError: An error from an invalid HTTP response. :return: Valid EndpointData or None if not available. :rtype: `keystoneauth1.discover.EndpointData` or None """ return super(FixedEndpointPlugin, self).get_endpoint_data( session, endpoint_override=endpoint_override or self.endpoint, discover_versions=discover_versions, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/service_token.py0000664000175000017500000000552700000000000022375 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import plugin SERVICE_AUTH_HEADER_NAME = 'X-Service-Token' __all__ = ('ServiceTokenAuthWrapper',) class ServiceTokenAuthWrapper(plugin.BaseAuthPlugin): def __init__(self, user_auth, service_auth): self.user_auth = user_auth self.service_auth = service_auth def get_headers(self, session, **kwargs): headers = self.user_auth.get_headers(session, **kwargs) token = self.service_auth.get_token(session, **kwargs) headers[SERVICE_AUTH_HEADER_NAME] = token return headers def invalidate(self): # NOTE(jamielennox): hmm, what to do here? Should we invalidate both # the service and user auth? Only one? There's no way to know what the # failure was to selectively invalidate. user = self.user_auth.invalidate() service = self.service_auth.invalidate() return user or service def get_connection_params(self, *args, **kwargs): # NOTE(jamielennox): This is also a bit of a guess but unlikely to be a # problem in practice. We don't know how merging connection parameters # between these plugins will conflict - but there aren't many plugins # that set this anyway. # Take the service auth params first so that user auth params will be # given priority. params = self.service_auth.get_connection_params(*args, **kwargs) params.update(self.user_auth.get_connection_params(*args, **kwargs)) return params # TODO(jamielennox): Everything below here is a generic wrapper that could # be extracted into a base wrapper class. We can do this as soon as there # is a need for it, but we may never actually need it. def get_token(self, *args, **kwargs): return self.user_auth.get_token(*args, **kwargs) def get_endpoint(self, *args, **kwargs): return self.user_auth.get_endpoint(*args, **kwargs) def get_user_id(self, *args, **kwargs): return self.user_auth.get_user_id(*args, **kwargs) def get_project_id(self, *args, **kwargs): return self.user_auth.get_project_id(*args, **kwargs) def get_sp_auth_url(self, *args, **kwargs): return self.user_auth.get_sp_auth_url(*args, **kwargs) def get_sp_url(self, *args, **kwargs): return self.user_auth.get_sp_url(*args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/session.py0000664000175000017500000017353500000000000021225 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime import functools import hashlib import json import logging import os import platform import socket import sys import time import uuid import requests import six from six.moves import urllib import keystoneauth1 from keystoneauth1 import _utils as utils from keystoneauth1 import discover from keystoneauth1 import exceptions try: import netaddr except ImportError: netaddr = None try: import osprofiler.web as osprofiler_web except ImportError: osprofiler_web = None DEFAULT_USER_AGENT = 'keystoneauth1/%s %s %s/%s' % ( keystoneauth1.__version__, requests.utils.default_user_agent(), platform.python_implementation(), platform.python_version()) # NOTE(jamielennox): Clients will likely want to print more than json. Please # propose a patch if you have a content type you think is reasonable to print # here and we'll add it to the list as required. _LOG_CONTENT_TYPES = set(['application/json']) _MAX_RETRY_INTERVAL = 60.0 _EXPONENTIAL_DELAY_START = 0.5 # NOTE(efried): This is defined in oslo_middleware.request_id.INBOUND_HEADER, # but it didn't seem worth adding oslo_middleware to requirements just for that _REQUEST_ID_HEADER = 'X-Openstack-Request-Id' def _construct_session(session_obj=None): # NOTE(morganfainberg): if the logic in this function changes be sure to # update the betamax fixture's '_construct_session_with_betamax" function # as well. if not session_obj: session_obj = requests.Session() # Use TCPKeepAliveAdapter to fix bug 1323862 for scheme in list(session_obj.adapters): session_obj.mount(scheme, TCPKeepAliveAdapter()) return session_obj def _mv_legacy_headers_for_service(mv_service_type): """Workaround for services that predate standardization. TODO(sdague): eventually convert this to using os-service-types and put the logic there. However, right now this is so little logic, inlining it for release is a better call. """ headers = [] if mv_service_type == "compute": headers.append("X-OpenStack-Nova-API-Version") elif mv_service_type == "baremetal": headers.append("X-OpenStack-Ironic-API-Version") elif mv_service_type in ["sharev2", "shared-file-system"]: headers.append("X-OpenStack-Manila-API-Version") return headers def _sanitize_headers(headers): """Ensure headers are strings and not bytes.""" str_dict = {} for k, v in headers.items(): if six.PY3: # requests expects headers to be str type in python3, which means # if we get a bytes we need to decode it into a str k = k.decode('ASCII') if isinstance(k, six.binary_type) else k if v is not None: v = v.decode('ASCII') if isinstance(v, six.binary_type) else v else: # requests expects headers to be str type in python2, which means # if we get a unicode we need to encode it to ASCII into a str k = k.encode('ASCII') if isinstance(k, six.text_type) else k if v is not None: v = v.encode('ASCII') if isinstance(v, six.text_type) else v str_dict[k] = v return str_dict class NoOpSemaphore(object): """Empty context manager for use as a default semaphore.""" def __enter__(self): """Enter the context manager and do nothing.""" pass def __exit__(self, exc_type, exc_value, traceback): """Exit the context manager and do nothing.""" pass class _JSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, datetime.datetime): return o.isoformat() if isinstance(o, uuid.UUID): return six.text_type(o) if netaddr and isinstance(o, netaddr.IPAddress): return six.text_type(o) return super(_JSONEncoder, self).default(o) class _StringFormatter(object): """A String formatter that fetches values on demand.""" def __init__(self, session, auth): self.session = session self.auth = auth def __getitem__(self, item): if item == 'project_id': value = self.session.get_project_id(self.auth) elif item == 'user_id': value = self.session.get_user_id(self.auth) else: raise AttributeError(item) if not value: raise ValueError("This type of authentication does not provide a " "%s that can be substituted" % item) return value def _determine_calling_package(): """Walk the call frames trying to identify what is using this module.""" # Create a lookup table mapping file name to module name. The ``inspect`` # module does this but is far less efficient. Same story with the # frame walking below. One could use ``inspect.stack()`` but it # has far more overhead. mod_lookup = dict((m.__file__, n) for n, m in sys.modules.items() if hasattr(m, '__file__')) # NOTE(shaleh): these are not useful because they hide the real # user of the code. debtcollector did not import keystoneauth but # it will show up in the call stack. Similarly we do not want to # report ourselves or keystone client as the user agent. The real # user is the code importing them. ignored = ('debtcollector', 'keystoneauth1', 'keystoneclient') i = 0 while True: i += 1 try: # NOTE(shaleh): this is safe in CPython but could break in # other implementations of Python. Yes, the `inspect` # module could be used instead. But it does a lot more # work so it has worse performance. f = sys._getframe(i) try: name = mod_lookup[f.f_code.co_filename] # finds the full name module.foo.bar but all we need # is the module name. name, _, _ = name.partition('.') if name not in ignored: return name except KeyError: pass # builtin or the like except ValueError: # hit the bottom of the frame stack break return '' def _determine_user_agent(): """Attempt to programmatically generate a user agent string. First, look at the name of the process. Return this unless it is in the `ignored` list. Otherwise, look at the function call stack and try to find the name of the code that invoked this module. """ # NOTE(shaleh): mod_wsgi is not any more useful than just # reporting "keystoneauth". Ignore it and perform the package name # heuristic. ignored = ('mod_wsgi', ) try: name = sys.argv[0] except IndexError: # sys.argv is empty, usually the Python interpreter prevents this. return '' if not name: return '' name = os.path.basename(name) if name in ignored: name = _determine_calling_package() return name class RequestTiming(object): """Contains timing information for an HTTP interaction.""" #: HTTP method used for the call (GET, POST, etc) method = None #: URL against which the call was made url = None #: Elapsed time information elapsed = None # type: datetime.timedelta def __init__(self, method, url, elapsed): self.method = method self.url = url self.elapsed = elapsed class _Retries(object): __slots__ = ('_fixed_delay', '_current') def __init__(self, fixed_delay=None): self._fixed_delay = fixed_delay self.reset() def __next__(self): value = self._current if not self._fixed_delay: self._current = min(value * 2, _MAX_RETRY_INTERVAL) return value def reset(self): if self._fixed_delay: self._current = self._fixed_delay else: self._current = _EXPONENTIAL_DELAY_START # Python 2 compatibility next = __next__ class Session(object): """Maintains client communication state and common functionality. As much as possible the parameters to this class reflect and are passed directly to the :mod:`requests` library. :param auth: An authentication plugin to authenticate the session with. (optional, defaults to None) :type auth: keystoneauth1.plugin.BaseAuthPlugin :param requests.Session session: A requests session object that can be used for issuing requests. (optional) :param str original_ip: The original IP of the requesting user which will be sent to identity service in a 'Forwarded' header. (optional) :param verify: The verification arguments to pass to requests. These are of the same form as requests expects, so True or False to verify (or not) against system certificates or a path to a bundle or CA certs to check against or None for requests to attempt to locate and use certificates. (optional, defaults to True) :param cert: A client certificate to pass to requests. These are of the same form as requests expects. Either a single filename containing both the certificate and key or a tuple containing the path to the certificate then a path to the key. (optional) :param float timeout: A timeout to pass to requests. This should be a numerical value indicating some amount (or fraction) of seconds or 0 for no timeout. (optional, defaults to 0) :param str user_agent: A User-Agent header string to use for the request. If not provided, a default of :attr:`~keystoneauth1.session.DEFAULT_USER_AGENT` is used, which contains the keystoneauth1 version as well as those of the requests library and which Python is being used. When a non-None value is passed, it will be prepended to the default. :param int/bool redirect: Controls the maximum number of redirections that can be followed by a request. Either an integer for a specific count or True/False for forever/never. (optional, default to 30) :param dict additional_headers: Additional headers that should be attached to every request passing through the session. Headers of the same name specified per request will take priority. :param str app_name: The name of the application that is creating the session. This will be used to create the user_agent. :param str app_version: The version of the application creating the session. This will be used to create the user_agent. :param list additional_user_agent: A list of tuple of name, version that will be added to the user agent. This can be used by libraries that are part of the communication process. :param dict discovery_cache: A dict to be used for caching of discovery information. This is normally managed transparently, but if the user wants to share a single cache across multiple sessions that do not share an auth plugin, it can be provided here. (optional, defaults to None which means automatically manage) :param bool split_loggers: Split the logging of requests across multiple loggers instead of just one. Defaults to False. :param bool collect_timing: Whether or not to collect per-method timing information for each API call. (optional, defaults to False) :param rate_semaphore: Semaphore to be used to control concurrency and rate limiting of requests. (optional, defaults to no concurrency or rate control) :param int connect_retries: the maximum number of retries that should be attempted for connection errors. (optional, defaults to 0 - never retry). """ user_agent = None _REDIRECT_STATUSES = (301, 302, 303, 305, 307, 308) _DEFAULT_REDIRECT_LIMIT = 30 def __init__(self, auth=None, session=None, original_ip=None, verify=True, cert=None, timeout=None, user_agent=None, redirect=_DEFAULT_REDIRECT_LIMIT, additional_headers=None, app_name=None, app_version=None, additional_user_agent=None, discovery_cache=None, split_loggers=None, collect_timing=False, rate_semaphore=None, connect_retries=0): self.auth = auth self.session = _construct_session(session) # NOTE(mwhahaha): keep a reference to the session object so we can # clean it up when this object goes away. We don't want to close the # session if it was passed into us as it may be reused externally. # See LP#1838704 self._session = None if not session: self._session = self.session self.original_ip = original_ip self.verify = verify self.cert = cert self.timeout = None self.redirect = redirect self.additional_headers = additional_headers or {} self.app_name = app_name self.app_version = app_version self.additional_user_agent = additional_user_agent or [] self._determined_user_agent = None if discovery_cache is None: discovery_cache = {} self._discovery_cache = discovery_cache # NOTE(mordred) split_loggers kwarg default is None rather than False # so we can distinguish between the value being set or not. self._split_loggers = split_loggers self._collect_timing = collect_timing self._connect_retries = connect_retries self._api_times = [] self._rate_semaphore = rate_semaphore or NoOpSemaphore() if timeout is not None: self.timeout = float(timeout) if user_agent is not None: self.user_agent = "%s %s" % (user_agent, DEFAULT_USER_AGENT) self._json = _JSONEncoder() def __del__(self): """Clean up resources on delete.""" if self._session: # If we created a requests.Session, try to close it out correctly try: self._session.close() except Exception: pass finally: self._session = None @property def adapters(self): return self.session.adapters @adapters.setter def adapters(self, value): self.session.adapters = value def mount(self, scheme, adapter): self.session.mount(scheme, adapter) def _remove_service_catalog(self, body): try: data = json.loads(body) # V3 token if 'token' in data and 'catalog' in data['token']: data['token']['catalog'] = '' return self._json.encode(data) # V2 token if 'serviceCatalog' in data['access']: data['access']['serviceCatalog'] = '' return self._json.encode(data) except Exception: # Don't fail trying to clean up the request body. pass return body @staticmethod def _process_header(header): """Redact the secure headers to be logged.""" secure_headers = ('authorization', 'x-auth-token', 'x-subject-token', 'x-service-token') if header[0].lower() in secure_headers: token_hasher = hashlib.sha256() token_hasher.update(header[1].encode('utf-8')) token_hash = token_hasher.hexdigest() return (header[0], '{SHA256}%s' % token_hash) return header def _get_split_loggers(self, split_loggers): """Get a boolean value from the various argument sources. We default split_loggers to None in the kwargs of the Session constructor so we can track set vs. not set. We also accept split_loggers as a parameter in a few other places. In each place we want the parameter, if given by the user, to win. """ # None is the default value in each method's kwarg. None means "unset". if split_loggers is None: # If no value was given, try the value set on the instance. split_loggers = self._split_loggers if split_loggers is None: # If neither a value was given on the method, nor a value was given # on the Session constructor, then the value is False. split_loggers = False return split_loggers def _http_log_request(self, url, method=None, data=None, json=None, headers=None, query_params=None, logger=None, split_loggers=None): string_parts = [] if self._get_split_loggers(split_loggers): logger = utils.get_logger(__name__ + '.request') else: # Only a single logger was passed in, use string prefixing. string_parts.append('REQ:') if not logger: logger = utils.get_logger(__name__) if not logger.isEnabledFor(logging.DEBUG): # NOTE(morganfainberg): This whole debug section is expensive, # there is no need to do the work if we're not going to emit a # debug log. return string_parts.append('curl -g -i') # NOTE(jamielennox): None means let requests do its default validation # so we need to actually check that this is False. if self.verify is False: string_parts.append('--insecure') elif isinstance(self.verify, six.string_types): string_parts.append('--cacert "%s"' % self.verify) if method: string_parts.extend(['-X', method]) if query_params: # Don't check against `is not None` as this can be # an empty dictionary, which we shouldn't bother logging. url = url + '?' + urllib.parse.urlencode(query_params) # URLs with query strings need to be wrapped in quotes in order # for the CURL command to run properly. string_parts.append('"%s"' % url) else: string_parts.append(url) if headers: # Sort headers so that testing can work consistently. for header in sorted(headers.items()): string_parts.append('-H "%s: %s"' % self._process_header(header)) if json: data = self._json.encode(json) if data: if isinstance(data, six.binary_type): try: data = data.decode("ascii") except UnicodeDecodeError: data = "" string_parts.append("-d '%s'" % data) logger.debug(' '.join(string_parts)) def _http_log_response(self, response=None, json=None, status_code=None, headers=None, text=None, logger=None, split_loggers=True): string_parts = [] body_parts = [] if self._get_split_loggers(split_loggers): logger = utils.get_logger(__name__ + '.response') body_logger = utils.get_logger(__name__ + '.body') else: # Only a single logger was passed in, use string prefixing. string_parts.append('RESP:') body_parts.append('RESP BODY:') body_logger = logger if not logger.isEnabledFor(logging.DEBUG): return if response is not None: if not status_code: status_code = response.status_code if not headers: headers = response.headers if status_code: string_parts.append('[%s]' % status_code) if headers: # Sort headers so that testing can work consistently. for header in sorted(headers.items()): string_parts.append('%s: %s' % self._process_header(header)) logger.debug(' '.join(string_parts)) if not body_logger.isEnabledFor(logging.DEBUG): return if response is not None: if not text: # NOTE(samueldmq): If the response does not provide enough info # about the content type to decide whether it is useful and # safe to log it or not, just do not log the body. Trying to # read the response body anyways may result on reading a long # stream of bytes and getting an unexpected MemoryError. See # bug 1616105 for further details. content_type = response.headers.get('content-type', None) # NOTE(lamt): Per [1], the Content-Type header can be of the # form Content-Type := type "/" subtype *[";" parameter] # [1] https://www.w3.org/Protocols/rfc1341/4_Content-Type.html for log_type in _LOG_CONTENT_TYPES: if content_type is not None and content_type.startswith( log_type): text = self._remove_service_catalog(response.text) break else: text = ('Omitted, Content-Type is set to %s. Only ' '%s responses have their bodies logged.') text = text % (content_type, ', '.join(_LOG_CONTENT_TYPES)) if json: text = self._json.encode(json) if text: body_parts.append(text) body_logger.debug(' '.join(body_parts)) @staticmethod def _set_microversion_headers( headers, microversion, service_type, endpoint_filter): # We're converting it to normalized version number for two reasons. # First, to validate it's a real version number. Second, so that in # the future we can pre-validate that it is within the range of # available microversions before we send the request. # TODO(mordred) Validate when we get the response back that # the server executed in the microversion we expected. # TODO(mordred) Validate that the requested microversion works # with the microversion range we found in discovery. microversion = discover.normalize_version_number(microversion) # Can't specify a M.latest microversion if (microversion[0] != discover.LATEST and discover.LATEST in microversion[1:]): raise TypeError( "Specifying a '{major}.latest' microversion is not allowed.") microversion = discover.version_to_string(microversion) if not service_type: if endpoint_filter and 'service_type' in endpoint_filter: service_type = endpoint_filter['service_type'] else: raise TypeError( "microversion {microversion} was requested but no" " service_type information is available. Either provide a" " service_type in endpoint_filter or pass" " microversion_service_type as an argument.".format( microversion=microversion)) # TODO(mordred) cinder uses volume in its microversion header. This # logic should be handled in the future by os-service-types but for # now hard-code for cinder. if (service_type.startswith('volume') or service_type == 'block-storage'): service_type = 'volume' elif service_type.startswith('share'): # NOTE(gouthamr) manila doesn't honor the "OpenStack-API-Version" # header yet, but sending it does no harm - when the service # honors this header, it'll use the standardized name in the # service-types-authority and not the legacy name in the cloud's # service catalog service_type = 'shared-file-system' headers.setdefault('OpenStack-API-Version', '{service_type} {microversion}'.format( service_type=service_type, microversion=microversion)) header_names = _mv_legacy_headers_for_service(service_type) for h in header_names: headers.setdefault(h, microversion) def request(self, url, method, json=None, original_ip=None, user_agent=None, redirect=None, authenticated=None, endpoint_filter=None, auth=None, requests_auth=None, raise_exc=True, allow_reauth=True, log=True, endpoint_override=None, connect_retries=None, logger=None, allow=None, client_name=None, client_version=None, microversion=None, microversion_service_type=None, status_code_retries=0, retriable_status_codes=None, rate_semaphore=None, global_request_id=None, connect_retry_delay=None, status_code_retry_delay=None, **kwargs): """Send an HTTP request with the specified characteristics. Wrapper around `requests.Session.request` to handle tasks such as setting headers, JSON encoding/decoding, and error handling. Arguments that are not handled are passed through to the requests library. :param str url: Path or fully qualified URL of HTTP request. If only a path is provided then endpoint_filter must also be provided such that the base URL can be determined. If a fully qualified URL is provided then endpoint_filter will be ignored. :param str method: The http method to use. (e.g. 'GET', 'POST') :param str original_ip: Mark this request as forwarded for this ip. (optional) :param dict headers: Headers to be included in the request. (optional) :param json: Some data to be represented as JSON. (optional) :param str user_agent: A user_agent to use for the request. If present will override one present in headers. (optional) :param int/bool redirect: the maximum number of redirections that can be followed by a request. Either an integer for a specific count or True/False for forever/never. (optional) :param int connect_retries: the maximum number of retries that should be attempted for connection errors. (optional, defaults to None - never retry). :param bool authenticated: True if a token should be attached to this request, False if not or None for attach if an auth_plugin is available. (optional, defaults to None) :param dict endpoint_filter: Data to be provided to an auth plugin with which it should be able to determine an endpoint to use for this request. If not provided then URL is expected to be a fully qualified URL. (optional) :param str endpoint_override: The URL to use instead of looking up the endpoint in the auth plugin. This will be ignored if a fully qualified URL is provided but take priority over an endpoint_filter. This string may contain the values ``%(project_id)s`` and ``%(user_id)s`` to have those values replaced by the project_id/user_id of the current authentication. (optional) :param auth: The auth plugin to use when authenticating this request. This will override the plugin that is attached to the session (if any). (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :param requests_auth: A requests library auth plugin that cannot be passed via kwarg because the `auth` kwarg collides with our own auth plugins. (optional) :type requests_auth: :py:class:`requests.auth.AuthBase` :param bool raise_exc: If True then raise an appropriate exception for failed HTTP requests. If False then return the request object. (optional, default True) :param bool allow_reauth: Allow fetching a new token and retrying the request on receiving a 401 Unauthorized response. (optional, default True) :param bool log: If True then log the request and response data to the debug log. (optional, default True) :param logger: The logger object to use to log request and responses. If not provided the keystoneauth1.session default logger will be used. :type logger: logging.Logger :param dict allow: Extra filters to pass when discovering API versions. (optional) :param microversion: Microversion to send for this request. microversion can be given as a string or a tuple. (optional) :param str microversion_service_type: The service_type to be sent in the microversion header, if a microversion is given. Defaults to the value of service_type from endpoint_filter if one exists. If endpoint_filter is not provided or does not have a service_type, microversion is given and microversion_service_type is not provided, an exception will be raised. :param int status_code_retries: the maximum number of retries that should be attempted for retriable HTTP status codes (optional, defaults to 0 - never retry). :param list retriable_status_codes: list of HTTP status codes that should be retried (optional, defaults to HTTP 503, has no effect when status_code_retries is 0). :param rate_semaphore: Semaphore to be used to control concurrency and rate limiting of requests. (optional, defaults to no concurrency or rate control) :param global_request_id: Value for the X-Openstack-Request-Id header. :param float connect_retry_delay: Delay (in seconds) between two connect retries (if enabled). By default exponential retry starting with 0.5 seconds up to a maximum of 60 seconds is used. :param float status_code_retry_delay: Delay (in seconds) between two status code retries (if enabled). By default exponential retry starting with 0.5 seconds up to a maximum of 60 seconds is used. :param kwargs: any other parameter that can be passed to :meth:`requests.Session.request` (such as `headers`). Except: - `data` will be overwritten by the data in the `json` param. - `allow_redirects` is ignored as redirects are handled by the session. :raises keystoneauth1.exceptions.base.ClientException: For connection failure, or to indicate an error response code. :returns: The response to the request. """ # If a logger is passed in, use it and do not log requests, responses # and bodies separately. if logger: split_loggers = False else: split_loggers = None logger = logger or utils.get_logger(__name__) # NOTE(gmann): Convert r initlize the headers to # CaseInsensitiveDict to make sure headers are # case insensitive. if kwargs.get('headers'): kwargs['headers'] = requests.structures.CaseInsensitiveDict( kwargs['headers']) else: kwargs['headers'] = requests.structures.CaseInsensitiveDict() if connect_retries is None: connect_retries = self._connect_retries # HTTP 503 - Service Unavailable retriable_status_codes = retriable_status_codes or [503] rate_semaphore = rate_semaphore or self._rate_semaphore headers = kwargs.setdefault('headers', dict()) if microversion: self._set_microversion_headers( headers, microversion, microversion_service_type, endpoint_filter) if authenticated is None: authenticated = bool(auth or self.auth) if authenticated: auth_headers = self.get_auth_headers(auth) if auth_headers is None: msg = 'No valid authentication is available' raise exceptions.AuthorizationFailure(msg) headers.update(auth_headers) if osprofiler_web: headers.update(osprofiler_web.get_trace_id_headers()) # if we are passed a fully qualified URL and an endpoint_filter we # should ignore the filter. This will make it easier for clients who # want to overrule the default endpoint_filter data added to all client # requests. We check fully qualified here by the presence of a host. if not urllib.parse.urlparse(url).netloc: base_url = None if endpoint_override: base_url = endpoint_override % _StringFormatter(self, auth) elif endpoint_filter: base_url = self.get_endpoint(auth, allow=allow, **endpoint_filter) if not base_url: raise exceptions.EndpointNotFound() url = '%s/%s' % (base_url.rstrip('/'), url.lstrip('/')) if self.cert: kwargs.setdefault('cert', self.cert) if self.timeout is not None: kwargs.setdefault('timeout', self.timeout) if user_agent: headers['User-Agent'] = user_agent elif self.user_agent: user_agent = headers.setdefault('User-Agent', self.user_agent) else: # Per RFC 7231 Section 5.5.3, identifiers in a user-agent should be # ordered by decreasing significance. If a user sets their product # that value will be used. Otherwise we attempt to derive a useful # product value. The value will be prepended it to the KSA version, # requests version, and then the Python version. agent = [] if self.app_name and self.app_version: agent.append('%s/%s' % (self.app_name, self.app_version)) elif self.app_name: agent.append(self.app_name) if client_name and client_version: agent.append('%s/%s' % (client_name, client_version)) elif client_name: agent.append(client_name) for additional in self.additional_user_agent: agent.append('%s/%s' % additional) if not agent: # NOTE(jamielennox): determine_user_agent will return an empty # string on failure so checking for None will ensure it is only # called once even on failure. if self._determined_user_agent is None: self._determined_user_agent = _determine_user_agent() if self._determined_user_agent: agent.append(self._determined_user_agent) agent.append(DEFAULT_USER_AGENT) user_agent = headers.setdefault('User-Agent', ' '.join(agent)) if self.original_ip: headers.setdefault('Forwarded', 'for=%s;by=%s' % (self.original_ip, user_agent)) if json is not None: headers.setdefault('Content-Type', 'application/json') kwargs['data'] = self._json.encode(json) if global_request_id is not None: # NOTE(efried): This does *not* setdefault. If a global_request_id # kwarg was explicitly specified, it should override any value # previously configured (e.g. in Adapter.global_request_id). headers[_REQUEST_ID_HEADER] = global_request_id for k, v in self.additional_headers.items(): headers.setdefault(k, v) # Bug #1766235: some headers may be bytes headers = _sanitize_headers(headers) kwargs['headers'] = headers kwargs.setdefault('verify', self.verify) if requests_auth: kwargs['auth'] = requests_auth # Query parameters that are included in the url string will # be logged properly, but those sent in the `params` parameter # (which the requests library handles) need to be explicitly # picked out so they can be included in the URL that gets loggged. query_params = kwargs.get('params', dict()) if log: self._http_log_request(url, method=method, data=kwargs.get('data'), headers=headers, query_params=query_params, logger=logger, split_loggers=split_loggers) # Force disable requests redirect handling. We will manage this below. kwargs['allow_redirects'] = False if redirect is None: redirect = self.redirect connect_retry_delays = _Retries(connect_retry_delay) status_code_retry_delays = _Retries(status_code_retry_delay) send = functools.partial(self._send_request, url, method, redirect, log, logger, split_loggers, connect_retries, status_code_retries, retriable_status_codes, rate_semaphore, connect_retry_delays, status_code_retry_delays) try: connection_params = self.get_auth_connection_params(auth=auth) except exceptions.MissingAuthPlugin: # NOTE(jamielennox): If we've gotten this far without an auth # plugin then we should be happy with allowing no additional # connection params. This will be the typical case for plugins # anyway. pass else: if connection_params: kwargs.update(connection_params) resp = send(**kwargs) # log callee and caller request-id for each api call if log: # service_name should be fetched from endpoint_filter if it is not # present then use service_type as service_name. service_name = None if endpoint_filter: service_name = endpoint_filter.get('service_name') if not service_name: service_name = endpoint_filter.get('service_type') # Nova uses 'x-compute-request-id' and other services like # Glance, Cinder etc are using 'x-openstack-request-id' to store # request-id in the header request_id = (resp.headers.get('x-openstack-request-id') or resp.headers.get('x-compute-request-id')) if request_id: if self._get_split_loggers(split_loggers): id_logger = utils.get_logger(__name__ + '.request-id') else: id_logger = logger if service_name: id_logger.debug( '%(method)s call to %(service_name)s for ' '%(url)s used request id ' '%(response_request_id)s', { 'method': resp.request.method, 'service_name': service_name, 'url': resp.url, 'response_request_id': request_id }) else: id_logger.debug( '%(method)s call to ' '%(url)s used request id ' '%(response_request_id)s', { 'method': resp.request.method, 'url': resp.url, 'response_request_id': request_id }) # handle getting a 401 Unauthorized response by invalidating the plugin # and then retrying the request. This is only tried once. if resp.status_code == 401 and authenticated and allow_reauth: if self.invalidate(auth): auth_headers = self.get_auth_headers(auth) if auth_headers is not None: headers.update(auth_headers) resp = send(**kwargs) if raise_exc and resp.status_code >= 400: logger.debug('Request returned failure status: %s', resp.status_code) raise exceptions.from_response(resp, method, url) if self._collect_timing: for h in resp.history: self._api_times.append(RequestTiming( method=h.request.method, url=h.request.url, elapsed=h.elapsed, )) self._api_times.append(RequestTiming( method=resp.request.method, url=resp.request.url, elapsed=resp.elapsed, )) return resp def _send_request(self, url, method, redirect, log, logger, split_loggers, connect_retries, status_code_retries, retriable_status_codes, rate_semaphore, connect_retry_delays, status_code_retry_delays, **kwargs): # NOTE(jamielennox): We handle redirection manually because the # requests lib follows some browser patterns where it will redirect # POSTs as GETs for certain statuses which is not want we want for an # API. See: https://en.wikipedia.org/wiki/Post/Redirect/Get # NOTE(jamielennox): The interaction between retries and redirects are # handled naively. We will attempt only a maximum number of retries and # redirects rather than per request limits. Otherwise the extreme case # could be redirects * retries requests. This will be sufficient in # most cases and can be fixed properly if there's ever a need. try: try: with rate_semaphore: resp = self.session.request(method, url, **kwargs) except requests.exceptions.SSLError as e: msg = 'SSL exception connecting to %(url)s: %(error)s' % { 'url': url, 'error': e} raise exceptions.SSLError(msg) except requests.exceptions.Timeout: msg = 'Request to %s timed out' % url raise exceptions.ConnectTimeout(msg) except requests.exceptions.ConnectionError as e: # NOTE(sdague): urllib3/requests connection error is a # translation of SocketError. However, SocketError # happens for many different reasons, and that low # level message is often really important in figuring # out the difference between network misconfigurations # and firewall blocking. msg = 'Unable to establish connection to %s: %s' % (url, e) raise exceptions.ConnectFailure(msg) except requests.exceptions.RequestException as e: msg = 'Unexpected exception for %(url)s: %(error)s' % { 'url': url, 'error': e} raise exceptions.UnknownConnectionError(msg, e) except exceptions.RetriableConnectionFailure as e: if connect_retries <= 0: raise delay = next(connect_retry_delays) logger.info('Failure: %(e)s. Retrying in %(delay).1fs.', {'e': e, 'delay': delay}) time.sleep(delay) return self._send_request( url, method, redirect, log, logger, split_loggers, status_code_retries=status_code_retries, retriable_status_codes=retriable_status_codes, rate_semaphore=rate_semaphore, connect_retries=connect_retries - 1, connect_retry_delays=connect_retry_delays, status_code_retry_delays=status_code_retry_delays, **kwargs) if log: self._http_log_response( response=resp, logger=logger, split_loggers=split_loggers) if resp.status_code in self._REDIRECT_STATUSES: # be careful here in python True == 1 and False == 0 if isinstance(redirect, bool): redirect_allowed = redirect else: redirect -= 1 redirect_allowed = redirect >= 0 if not redirect_allowed: return resp try: location = resp.headers['location'] except KeyError: logger.warning("Failed to redirect request to %s as new " "location was not provided.", resp.url) else: # NOTE(jamielennox): We don't keep increasing delays. # This request actually worked so we can reset the delay count. connect_retry_delays.reset() status_code_retry_delays.reset() new_resp = self._send_request( location, method, redirect, log, logger, split_loggers, rate_semaphore=rate_semaphore, connect_retries=connect_retries, status_code_retries=status_code_retries, retriable_status_codes=retriable_status_codes, connect_retry_delays=connect_retry_delays, status_code_retry_delays=status_code_retry_delays, **kwargs) if not isinstance(new_resp.history, list): new_resp.history = list(new_resp.history) new_resp.history.insert(0, resp) resp = new_resp elif (resp.status_code in retriable_status_codes and status_code_retries > 0): delay = next(status_code_retry_delays) logger.info('Retriable status code %(code)s. Retrying in ' '%(delay).1fs.', {'code': resp.status_code, 'delay': delay}) time.sleep(delay) # NOTE(jamielennox): We don't keep increasing connection delays. # This request actually worked so we can reset the delay count. connect_retry_delays.reset() return self._send_request( url, method, redirect, log, logger, split_loggers, connect_retries=connect_retries, status_code_retries=status_code_retries - 1, retriable_status_codes=retriable_status_codes, rate_semaphore=rate_semaphore, connect_retry_delays=connect_retry_delays, status_code_retry_delays=status_code_retry_delays, **kwargs) return resp def head(self, url, **kwargs): """Perform a HEAD request. This calls :py:meth:`.request()` with ``method`` set to ``HEAD``. """ return self.request(url, 'HEAD', **kwargs) def get(self, url, **kwargs): """Perform a GET request. This calls :py:meth:`.request()` with ``method`` set to ``GET``. """ return self.request(url, 'GET', **kwargs) def post(self, url, **kwargs): """Perform a POST request. This calls :py:meth:`.request()` with ``method`` set to ``POST``. """ return self.request(url, 'POST', **kwargs) def put(self, url, **kwargs): """Perform a PUT request. This calls :py:meth:`.request()` with ``method`` set to ``PUT``. """ return self.request(url, 'PUT', **kwargs) def delete(self, url, **kwargs): """Perform a DELETE request. This calls :py:meth:`.request()` with ``method`` set to ``DELETE``. """ return self.request(url, 'DELETE', **kwargs) def patch(self, url, **kwargs): """Perform a PATCH request. This calls :py:meth:`.request()` with ``method`` set to ``PATCH``. """ return self.request(url, 'PATCH', **kwargs) def _auth_required(self, auth, msg): if not auth: auth = self.auth if not auth: msg_fmt = 'An auth plugin is required to %s' raise exceptions.MissingAuthPlugin(msg_fmt % msg) return auth def get_auth_headers(self, auth=None, **kwargs): """Return auth headers as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth.AuthorizationFailure: if a new token fetch fails. :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :returns: Authentication headers or None for failure. :rtype: :class:`dict` """ auth = self._auth_required(auth, 'fetch a token') return auth.get_headers(self, **kwargs) def get_token(self, auth=None): """Return a token as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth.AuthorizationFailure: if a new token fetch fails. :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. .. warning:: **DEPRECATED**: This assumes that the only header that is used to authenticate a message is ``X-Auth-Token``. This may not be correct. Use :meth:`get_auth_headers` instead. :returns: A valid token. :rtype: string """ return (self.get_auth_headers(auth) or {}).get('X-Auth-Token') def get_endpoint(self, auth=None, **kwargs): """Get an endpoint as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :returns: An endpoint if available or None. :rtype: string """ if 'endpoint_override' in kwargs: return kwargs['endpoint_override'] auth = self._auth_required(auth, 'determine endpoint URL') return auth.get_endpoint(self, **kwargs) def get_endpoint_data(self, auth=None, **kwargs): """Get endpoint data as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :raises TypeError: If arguments are invalid :returns: Endpoint data if available or None. :rtype: keystoneauth1.discover.EndpointData """ auth = self._auth_required(auth, 'determine endpoint URL') return auth.get_endpoint_data(self, **kwargs) def get_api_major_version(self, auth=None, **kwargs): """Get the major API version as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :return: The major version of the API of the service discovered. :rtype: tuple or None """ auth = self._auth_required(auth, 'determine endpoint URL') return auth.get_api_major_version(self, **kwargs) def get_all_version_data(self, auth=None, interface='public', region_name=None, service_type=None, **kwargs): """Get version data for all services in the catalog. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :param interface: Type of endpoint to get version data for. Can be a single value or a list of values. A value of None indicates that all interfaces should be queried. (optional, defaults to public) :param string region_name: Region of endpoints to get version data for. A valueof None indicates that all regions should be queried. (optional, defaults to None) :param string service_type: Limit the version data to a single service. (optional, defaults to None) :returns: A dictionary keyed by region_name with values containing dictionaries keyed by interface with values being a list of `~keystoneauth1.discover.VersionData`. """ auth = self._auth_required(auth, 'determine endpoint URL') return auth.get_all_version_data( self, interface=interface, region_name=region_name, service_type=service_type, **kwargs) def get_auth_connection_params(self, auth=None, **kwargs): """Return auth connection params as provided by the auth plugin. An auth plugin may specify connection parameters to the request like providing a client certificate for communication. We restrict the values that may be returned from this function to prevent an auth plugin overriding values unrelated to connection parmeters. The values that are currently accepted are: - `cert`: a path to a client certificate, or tuple of client certificate and key pair that are used with this request. - `verify`: a boolean value to indicate verifying SSL certificates against the system CAs or a path to a CA file to verify with. These values are passed to the requests library and further information on accepted values may be found there. :param auth: The auth plugin to use for tokens. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth.AuthorizationFailure: if a new token fetch fails. :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :raises keystoneauth1.exceptions.auth_plugins.UnsupportedParameters: if the plugin returns a parameter that is not supported by this session. :returns: Authentication headers or None for failure. :rtype: :class:`dict` """ auth = self._auth_required(auth, 'fetch connection params') params = auth.get_connection_params(self, **kwargs) # NOTE(jamielennox): There needs to be some consensus on what # parameters are allowed to be modified by the auth plugin here. # Ideally I think it would be only the send() parts of the request # flow. For now lets just allow certain elements. params_copy = params.copy() for arg in ('cert', 'verify'): try: kwargs[arg] = params_copy.pop(arg) except KeyError: pass if params_copy: raise exceptions.UnsupportedParameters(list(params_copy.keys())) return params def invalidate(self, auth=None): """Invalidate an authentication plugin. :param auth: The auth plugin to invalidate. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin """ auth = self._auth_required(auth, 'validate') return auth.invalidate() def get_user_id(self, auth=None): """Return the authenticated user_id as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth.AuthorizationFailure: if a new token fetch fails. :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :returns: Current user_id or None if not supported by plugin. :rtype: :class:`str` """ auth = self._auth_required(auth, 'get user_id') return auth.get_user_id(self) def get_project_id(self, auth=None): """Return the authenticated project_id as provided by the auth plugin. :param auth: The auth plugin to use for token. Overrides the plugin on the session. (optional) :type auth: keystoneauth1.plugin.BaseAuthPlugin :raises keystoneauth1.exceptions.auth.AuthorizationFailure: if a new token fetch fails. :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a plugin is not available. :returns: Current project_id or None if not supported by plugin. :rtype: :class:`str` """ auth = self._auth_required(auth, 'get project_id') return auth.get_project_id(self) def get_timings(self): """Return collected API timing information. :returns: List of `RequestTiming` objects. """ return self._api_times def reset_timings(self): """Clear API timing information.""" self._api_times = [] REQUESTS_VERSION = tuple(int(v) for v in requests.__version__.split('.')) class TCPKeepAliveAdapter(requests.adapters.HTTPAdapter): """The custom adapter used to set TCP Keep-Alive on all connections. This Adapter also preserves the default behaviour of Requests which disables Nagle's Algorithm. See also: https://blogs.msdn.com/b/windowsazurestorage/archive/2010/06/25/nagle-s-algorithm-is-not-friendly-towards-small-requests.aspx """ def init_poolmanager(self, *args, **kwargs): if 'socket_options' not in kwargs and REQUESTS_VERSION >= (2, 4, 1): socket_options = [ # Keep Nagle's algorithm off (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), # Turn on TCP Keep-Alive (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), ] # Some operating systems (e.g., OSX) do not support setting # keepidle if hasattr(socket, 'TCP_KEEPIDLE'): socket_options += [ # Wait 60 seconds before sending keep-alive probes (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) ] # Windows subsystem for Linux does not support this feature if (hasattr(socket, 'TCP_KEEPCNT') and not utils.is_windows_linux_subsystem): socket_options += [ # Set the maximum number of keep-alive probes (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 4), ] if hasattr(socket, 'TCP_KEEPINTVL'): socket_options += [ # Send keep-alive probes every 15 seconds (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 15), ] # After waiting 60 seconds, and then sending a probe once every 15 # seconds 4 times, these options should ensure that a connection # hands for no longer than 2 minutes before a ConnectionError is # raised. kwargs['socket_options'] = socket_options super(TCPKeepAliveAdapter, self).init_poolmanager(*args, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2967944 keystoneauth1-4.4.0/keystoneauth1/tests/0000775000175000017500000000000000000000000020314 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/__init__.py0000664000175000017500000000000000000000000022413 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3007944 keystoneauth1-4.4.0/keystoneauth1/tests/unit/0000775000175000017500000000000000000000000021273 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/__init__.py0000664000175000017500000000000000000000000023372 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3047943 keystoneauth1-4.4.0/keystoneauth1/tests/unit/access/0000775000175000017500000000000000000000000022534 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/access/__init__.py0000664000175000017500000000000000000000000024633 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/access/test_v2_access.py0000664000175000017500000001760200000000000026023 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime import uuid from oslo_utils import timeutils from keystoneauth1 import access from keystoneauth1 import fixture from keystoneauth1.tests.unit import utils class AccessV2Test(utils.TestCase): def test_building_unscoped_accessinfo(self): token = fixture.V2Token(expires='2012-10-03T16:58:01Z') auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertFalse(auth_ref.has_service_catalog()) self.assertEqual(auth_ref.auth_token, token.token_id) self.assertEqual(auth_ref.username, token.user_name) self.assertEqual(auth_ref.user_id, token.user_id) self.assertEqual(auth_ref.role_ids, []) self.assertEqual(auth_ref.role_names, []) self.assertIsNone(auth_ref.tenant_name) self.assertIsNone(auth_ref.tenant_id) self.assertFalse(auth_ref.domain_scoped) self.assertFalse(auth_ref.project_scoped) self.assertFalse(auth_ref.trust_scoped) self.assertIsNone(auth_ref.project_domain_id) self.assertIsNone(auth_ref.project_domain_name) self.assertIsNone(auth_ref.user_domain_id) self.assertIsNone(auth_ref.user_domain_name) self.assertEqual(auth_ref.expires, token.expires) self.assertEqual(auth_ref.issued, token.issued) self.assertEqual(token.audit_id, auth_ref.audit_id) self.assertIsNone(auth_ref.audit_chain_id) self.assertIsNone(token.audit_chain_id) self.assertIsNone(auth_ref.bind) def test_will_expire_soon(self): token = fixture.V2Token() expires = timeutils.utcnow() + datetime.timedelta(minutes=5) token.expires = expires auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertFalse(auth_ref.will_expire_soon(stale_duration=120)) self.assertTrue(auth_ref.will_expire_soon(stale_duration=300)) self.assertFalse(auth_ref.will_expire_soon()) def test_building_scoped_accessinfo(self): token = fixture.V2Token() token.set_scope() s = token.add_service('identity') s.add_endpoint('http://url') role_data = token.add_role() auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertTrue(auth_ref.has_service_catalog()) self.assertEqual(auth_ref.auth_token, token.token_id) self.assertEqual(auth_ref.username, token.user_name) self.assertEqual(auth_ref.user_id, token.user_id) self.assertEqual(auth_ref.role_ids, [role_data['id']]) self.assertEqual(auth_ref.role_names, [role_data['name']]) self.assertEqual(auth_ref.tenant_name, token.tenant_name) self.assertEqual(auth_ref.tenant_id, token.tenant_id) self.assertEqual(auth_ref.tenant_name, auth_ref.project_name) self.assertEqual(auth_ref.tenant_id, auth_ref.project_id) self.assertIsNone(auth_ref.project_domain_id, 'default') self.assertIsNone(auth_ref.project_domain_name, 'Default') self.assertIsNone(auth_ref.user_domain_id, 'default') self.assertIsNone(auth_ref.user_domain_name, 'Default') self.assertTrue(auth_ref.project_scoped) self.assertFalse(auth_ref.domain_scoped) self.assertEqual(token.audit_id, auth_ref.audit_id) self.assertEqual(token.audit_chain_id, auth_ref.audit_chain_id) def test_diablo_token(self): diablo_token = { 'access': { 'token': { 'id': uuid.uuid4().hex, 'expires': '2020-01-01T00:00:10.000123Z', 'tenantId': 'tenant_id1', }, 'user': { 'id': 'user_id1', 'name': 'user_name1', 'roles': [ {'name': 'role1'}, {'name': 'role2'}, ], }, }, } auth_ref = access.create(body=diablo_token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertTrue(auth_ref) self.assertEqual(auth_ref.username, 'user_name1') self.assertEqual(auth_ref.project_id, 'tenant_id1') self.assertEqual(auth_ref.project_name, 'tenant_id1') self.assertIsNone(auth_ref.project_domain_id) self.assertIsNone(auth_ref.project_domain_name) self.assertIsNone(auth_ref.user_domain_id) self.assertIsNone(auth_ref.user_domain_name) self.assertEqual(auth_ref.role_names, ['role1', 'role2']) def test_grizzly_token(self): grizzly_token = { 'access': { 'token': { 'id': uuid.uuid4().hex, 'expires': '2020-01-01T00:00:10.000123Z', }, 'user': { 'id': 'user_id1', 'name': 'user_name1', 'tenantId': 'tenant_id1', 'tenantName': 'tenant_name1', 'roles': [ {'name': 'role1'}, {'name': 'role2'}, ], }, }, } auth_ref = access.create(body=grizzly_token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertEqual(auth_ref.project_id, 'tenant_id1') self.assertEqual(auth_ref.project_name, 'tenant_name1') self.assertIsNone(auth_ref.project_domain_id) self.assertIsNone(auth_ref.project_domain_name) self.assertIsNone(auth_ref.user_domain_id, 'default') self.assertIsNone(auth_ref.user_domain_name, 'Default') self.assertEqual(auth_ref.role_names, ['role1', 'role2']) def test_v2_roles(self): role_id = 'a' role_name = 'b' token = fixture.V2Token() token.set_scope() token.add_role(id=role_id, name=role_name) auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertEqual([role_id], auth_ref.role_ids) self.assertEqual([role_id], auth_ref._data['access']['metadata']['roles']) self.assertEqual([role_name], auth_ref.role_names) self.assertEqual([{'name': role_name}], auth_ref._data['access']['user']['roles']) def test_trusts(self): user_id = uuid.uuid4().hex trust_id = uuid.uuid4().hex token = fixture.V2Token(user_id=user_id, trust_id=trust_id) token.set_scope() token.add_role() auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertEqual(trust_id, auth_ref.trust_id) self.assertEqual(user_id, auth_ref.trustee_user_id) self.assertEqual(trust_id, token['access']['trust']['id']) def test_binding(self): token = fixture.V2Token() principal = uuid.uuid4().hex token.set_bind('kerberos', principal) auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertEqual({'kerberos': principal}, auth_ref.bind) def test_is_admin_project(self): token = fixture.V2Token() auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertIs(True, auth_ref.is_admin_project) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/access/test_v2_service_catalog.py0000664000175000017500000002554200000000000027716 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1.tests.unit import utils class ServiceCatalogTest(utils.TestCase): def setUp(self): super(ServiceCatalogTest, self).setUp() self.AUTH_RESPONSE_BODY = fixture.V2Token( token_id='ab48a9efdfedb23ty3494', expires='2010-11-01T03:32:15-05:00', tenant_id='345', tenant_name='My Project', user_id='123', user_name='jqsmith', audit_chain_id=uuid.uuid4().hex) self.AUTH_RESPONSE_BODY.add_role(id='234', name='compute:admin') role = self.AUTH_RESPONSE_BODY.add_role(id='235', name='object-store:admin') role['tenantId'] = '1' s = self.AUTH_RESPONSE_BODY.add_service('compute', 'Cloud Servers') endpoint = s.add_endpoint( public='https://compute.north.host/v1/1234', internal='https://compute.north.host/v1/1234', region='North') endpoint['tenantId'] = '1' endpoint['versionId'] = '1.0' endpoint['versionInfo'] = 'https://compute.north.host/v1.0/' endpoint['versionList'] = 'https://compute.north.host/' endpoint = s.add_endpoint( public='https://compute.north.host/v1.1/3456', internal='https://compute.north.host/v1.1/3456', region='North') endpoint['tenantId'] = '2' endpoint['versionId'] = '1.1' endpoint['versionInfo'] = 'https://compute.north.host/v1.1/' endpoint['versionList'] = 'https://compute.north.host/' s = self.AUTH_RESPONSE_BODY.add_service('object-store', 'Cloud Files') endpoint = s.add_endpoint(public='https://swift.north.host/v1/blah', internal='https://swift.north.host/v1/blah', region='South') endpoint['tenantId'] = '11' endpoint['versionId'] = '1.0' endpoint['versionInfo'] = 'uri' endpoint['versionList'] = 'uri' endpoint = s.add_endpoint( public='https://swift.north.host/v1.1/blah', internal='https://compute.north.host/v1.1/blah', region='South') endpoint['tenantId'] = '2' endpoint['versionId'] = '1.1' endpoint['versionInfo'] = 'https://swift.north.host/v1.1/' endpoint['versionList'] = 'https://swift.north.host/' s = self.AUTH_RESPONSE_BODY.add_service('image', 'Image Servers') s.add_endpoint(public='https://image.north.host/v1/', internal='https://image-internal.north.host/v1/', region='North') s.add_endpoint(public='https://image.south.host/v1/', internal='https://image-internal.south.host/v1/', region='South') def test_building_a_service_catalog(self): auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog self.assertEqual(sc.url_for(service_type='compute'), "https://compute.north.host/v1/1234") self.assertRaises(exceptions.EndpointNotFound, sc.url_for, region_name="South", service_type='compute') def test_service_catalog_endpoints(self): auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog public_ep = sc.get_endpoints(service_type='compute', interface='publicURL') self.assertEqual(public_ep['compute'][1]['tenantId'], '2') self.assertEqual(public_ep['compute'][1]['versionId'], '1.1') self.assertEqual(public_ep['compute'][1]['internalURL'], "https://compute.north.host/v1.1/3456") def test_service_catalog_empty(self): self.AUTH_RESPONSE_BODY['access']['serviceCatalog'] = [] auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) self.assertRaises(exceptions.EmptyCatalog, auth_ref.service_catalog.url_for, service_type='image', interface='internalURL') def test_service_catalog_get_endpoints_region_names(self): auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog endpoints = sc.get_endpoints(service_type='image', region_name='North') self.assertEqual(len(endpoints), 1) self.assertEqual(endpoints['image'][0]['publicURL'], 'https://image.north.host/v1/') endpoints = sc.get_endpoints(service_type='image', region_name='South') self.assertEqual(len(endpoints), 1) self.assertEqual(endpoints['image'][0]['publicURL'], 'https://image.south.host/v1/') endpoints = sc.get_endpoints(service_type='compute') self.assertEqual(len(endpoints['compute']), 2) endpoints = sc.get_endpoints(service_type='compute', region_name='North') self.assertEqual(len(endpoints['compute']), 2) endpoints = sc.get_endpoints(service_type='compute', region_name='West') self.assertEqual(len(endpoints['compute']), 0) def test_service_catalog_url_for_region_names(self): auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog url = sc.url_for(service_type='image', region_name='North') self.assertEqual(url, 'https://image.north.host/v1/') url = sc.url_for(service_type='image', region_name='South') self.assertEqual(url, 'https://image.south.host/v1/') self.assertRaises(exceptions.EndpointNotFound, sc.url_for, service_type='image', region_name='West') def test_servcie_catalog_get_url_region_names(self): auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog urls = sc.get_urls(service_type='image') self.assertEqual(len(urls), 2) urls = sc.get_urls(service_type='image', region_name='North') self.assertEqual(len(urls), 1) self.assertEqual(urls[0], 'https://image.north.host/v1/') urls = sc.get_urls(service_type='image', region_name='South') self.assertEqual(len(urls), 1) self.assertEqual(urls[0], 'https://image.south.host/v1/') urls = sc.get_urls(service_type='image', region_name='West') self.assertEqual(len(urls), 0) def test_service_catalog_service_name(self): auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog url = sc.url_for(service_name='Image Servers', interface='public', service_type='image', region_name='North') self.assertEqual('https://image.north.host/v1/', url) self.assertRaises(exceptions.EndpointNotFound, sc.url_for, service_name='Image Servers', service_type='compute') urls = sc.get_urls(service_type='image', service_name='Image Servers', interface='public') self.assertIn('https://image.north.host/v1/', urls) self.assertIn('https://image.south.host/v1/', urls) urls = sc.get_urls(service_type='image', service_name='Servers', interface='public') self.assertEqual(0, len(urls)) def test_service_catalog_multiple_service_types(self): token = fixture.V2Token() token.set_scope() for i in range(3): s = token.add_service('compute') s.add_endpoint(public='public-%d' % i, admin='admin-%d' % i, internal='internal-%d' % i, region='region-%d' % i) auth_ref = access.create(body=token) urls = auth_ref.service_catalog.get_urls(service_type='compute', interface='publicURL') self.assertEqual(set(['public-0', 'public-1', 'public-2']), set(urls)) urls = auth_ref.service_catalog.get_urls(service_type='compute', interface='publicURL', region_name='region-1') self.assertEqual(('public-1', ), urls) def test_service_catalog_endpoint_id(self): token = fixture.V2Token() token.set_scope() endpoint_id = uuid.uuid4().hex public_url = uuid.uuid4().hex s = token.add_service('compute') s.add_endpoint(public=public_url, id=endpoint_id) s.add_endpoint(public=uuid.uuid4().hex) auth_ref = access.create(body=token) # initially assert that we get back all our urls for a simple filter urls = auth_ref.service_catalog.get_urls(interface='public') self.assertEqual(2, len(urls)) urls = auth_ref.service_catalog.get_urls(endpoint_id=endpoint_id, interface='public') self.assertEqual((public_url, ), urls) # with bad endpoint_id nothing should be found urls = auth_ref.service_catalog.get_urls(endpoint_id=uuid.uuid4().hex, interface='public') self.assertEqual(0, len(urls)) # we ignore a service_id because v2 doesn't know what it is urls = auth_ref.service_catalog.get_urls(endpoint_id=endpoint_id, service_id=uuid.uuid4().hex, interface='public') self.assertEqual((public_url, ), urls) def test_service_catalog_without_service_type(self): token = fixture.V2Token() token.set_scope() public_urls = [] for i in range(0, 3): public_url = uuid.uuid4().hex public_urls.append(public_url) s = token.add_service(uuid.uuid4().hex) s.add_endpoint(public=public_url) auth_ref = access.create(body=token) urls = auth_ref.service_catalog.get_urls(service_type=None, interface='public') self.assertEqual(3, len(urls)) for p in public_urls: self.assertIn(p, urls) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/access/test_v3_access.py0000664000175000017500000002761000000000000026024 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime import uuid from oslo_utils import timeutils from keystoneauth1 import access from keystoneauth1 import fixture from keystoneauth1.tests.unit import utils class AccessV3Test(utils.TestCase): def test_building_unscoped_accessinfo(self): token = fixture.V3Token() token_id = uuid.uuid4().hex auth_ref = access.create(body=token, auth_token=token_id) self.assertIn('methods', auth_ref._data['token']) self.assertFalse(auth_ref.has_service_catalog()) self.assertNotIn('catalog', auth_ref._data['token']) self.assertEqual(token_id, auth_ref.auth_token) self.assertEqual(token.user_name, auth_ref.username) self.assertEqual(token.user_id, auth_ref.user_id) self.assertEqual(auth_ref.role_ids, []) self.assertEqual(auth_ref.role_names, []) self.assertIsNone(auth_ref.project_name) self.assertIsNone(auth_ref.project_id) self.assertFalse(auth_ref.domain_scoped) self.assertFalse(auth_ref.project_scoped) self.assertIsNone(auth_ref.project_is_domain) self.assertEqual(token.user_domain_id, auth_ref.user_domain_id) self.assertEqual(token.user_domain_name, auth_ref.user_domain_name) self.assertIsNone(auth_ref.project_domain_id) self.assertIsNone(auth_ref.project_domain_name) self.assertEqual(auth_ref.expires, timeutils.parse_isotime( token['token']['expires_at'])) self.assertEqual(auth_ref.issued, timeutils.parse_isotime( token['token']['issued_at'])) self.assertEqual(auth_ref.expires, token.expires) self.assertEqual(auth_ref.issued, token.issued) self.assertEqual(auth_ref.audit_id, token.audit_id) self.assertIsNone(auth_ref.audit_chain_id) self.assertIsNone(token.audit_chain_id) self.assertIsNone(auth_ref.bind) def test_will_expire_soon(self): expires = timeutils.utcnow() + datetime.timedelta(minutes=5) token = fixture.V3Token(expires=expires) auth_ref = access.create(body=token) self.assertFalse(auth_ref.will_expire_soon(stale_duration=120)) self.assertTrue(auth_ref.will_expire_soon(stale_duration=301)) self.assertFalse(auth_ref.will_expire_soon()) def test_building_system_scoped_assessinfo(self): token = fixture.V3Token() token.set_system_scope() s = token.add_service(type='identity') s.add_standard_endpoints(public='http://url') token_id = uuid.uuid4().hex auth_ref = access.create(body=token, auth_token=token_id) self.assertTrue(auth_ref) self.assertIn('methods', auth_ref._data['token']) self.assertIn('catalog', auth_ref._data['token']) self.assertTrue(auth_ref.has_service_catalog()) self.assertTrue(auth_ref._data['token']['catalog']) self.assertEqual(token_id, auth_ref.auth_token) self.assertEqual(token.user_name, auth_ref.username) self.assertEqual(token.user_id, auth_ref.user_id) self.assertEqual(token.role_ids, auth_ref.role_ids) self.assertEqual(token.role_names, auth_ref.role_names) self.assertEqual(token.domain_name, auth_ref.domain_name) self.assertEqual(token.domain_id, auth_ref.domain_id) self.assertEqual(token.user_domain_id, auth_ref.user_domain_id) self.assertEqual(token.user_domain_name, auth_ref.user_domain_name) self.assertIsNone(auth_ref.project_name) self.assertIsNone(auth_ref.project_id) self.assertIsNone(auth_ref.project_domain_id) self.assertIsNone(auth_ref.project_domain_name) self.assertIsNone(auth_ref.domain_name) self.assertIsNone(auth_ref.domain_id) self.assertEqual(token.system, auth_ref.system) self.assertTrue(auth_ref.system_scoped) self.assertFalse(auth_ref.domain_scoped) self.assertFalse(auth_ref.project_scoped) self.assertEqual(token.audit_id, auth_ref.audit_id) self.assertEqual(token.audit_chain_id, auth_ref.audit_chain_id) def test_building_domain_scoped_accessinfo(self): token = fixture.V3Token() token.set_domain_scope() s = token.add_service(type='identity') s.add_standard_endpoints(public='http://url') token_id = uuid.uuid4().hex auth_ref = access.create(body=token, auth_token=token_id) self.assertTrue(auth_ref) self.assertIn('methods', auth_ref._data['token']) self.assertIn('catalog', auth_ref._data['token']) self.assertTrue(auth_ref.has_service_catalog()) self.assertTrue(auth_ref._data['token']['catalog']) self.assertEqual(token_id, auth_ref.auth_token) self.assertEqual(token.user_name, auth_ref.username) self.assertEqual(token.user_id, auth_ref.user_id) self.assertEqual(token.role_ids, auth_ref.role_ids) self.assertEqual(token.role_names, auth_ref.role_names) self.assertEqual(token.domain_name, auth_ref.domain_name) self.assertEqual(token.domain_id, auth_ref.domain_id) self.assertIsNone(auth_ref.project_name) self.assertIsNone(auth_ref.project_id) self.assertEqual(token.user_domain_id, auth_ref.user_domain_id) self.assertEqual(token.user_domain_name, auth_ref.user_domain_name) self.assertIsNone(auth_ref.project_domain_id) self.assertIsNone(auth_ref.project_domain_name) self.assertTrue(auth_ref.domain_scoped) self.assertFalse(auth_ref.project_scoped) self.assertIsNone(auth_ref.project_is_domain) self.assertEqual(token.audit_id, auth_ref.audit_id) self.assertEqual(token.audit_chain_id, auth_ref.audit_chain_id) def test_building_project_scoped_accessinfo(self): token = fixture.V3Token() token.set_project_scope() s = token.add_service(type='identity') s.add_standard_endpoints(public='http://url') token_id = uuid.uuid4().hex auth_ref = access.create(body=token, auth_token=token_id) self.assertIn('methods', auth_ref._data['token']) self.assertIn('catalog', auth_ref._data['token']) self.assertTrue(auth_ref.has_service_catalog()) self.assertTrue(auth_ref._data['token']['catalog']) self.assertEqual(token_id, auth_ref.auth_token) self.assertEqual(token.user_name, auth_ref.username) self.assertEqual(token.user_id, auth_ref.user_id) self.assertEqual(token.role_ids, auth_ref.role_ids) self.assertEqual(token.role_names, auth_ref.role_names) self.assertIsNone(auth_ref.domain_name) self.assertIsNone(auth_ref.domain_id) self.assertEqual(token.project_name, auth_ref.project_name) self.assertEqual(token.project_id, auth_ref.project_id) self.assertEqual(auth_ref.tenant_name, auth_ref.project_name) self.assertEqual(auth_ref.tenant_id, auth_ref.project_id) self.assertEqual(token.project_domain_id, auth_ref.project_domain_id) self.assertEqual(token.project_domain_name, auth_ref.project_domain_name) self.assertEqual(token.user_domain_id, auth_ref.user_domain_id) self.assertEqual(token.user_domain_name, auth_ref.user_domain_name) self.assertFalse(auth_ref.domain_scoped) self.assertTrue(auth_ref.project_scoped) self.assertIsNone(auth_ref.project_is_domain) self.assertEqual(token.audit_id, auth_ref.audit_id) self.assertEqual(token.audit_chain_id, auth_ref.audit_chain_id) def test_building_project_as_domain_scoped_accessinfo(self): token = fixture.V3Token() token.set_project_scope(is_domain=True) service = token.add_service(type='identity') service.add_standard_endpoints(public='http://url') token_id = uuid.uuid4().hex auth_ref = access.create(body=token, auth_token=token_id) self.assertIn('methods', auth_ref._data['token']) self.assertIn('catalog', auth_ref._data['token']) self.assertTrue(auth_ref.has_service_catalog()) self.assertTrue(auth_ref._data['token']['catalog']) self.assertEqual(token_id, auth_ref.auth_token) self.assertEqual(token.user_name, auth_ref.username) self.assertEqual(token.user_id, auth_ref.user_id) self.assertEqual(token.role_ids, auth_ref.role_ids) self.assertEqual(token.role_names, auth_ref.role_names) self.assertIsNone(auth_ref.domain_name) self.assertIsNone(auth_ref.domain_id) self.assertEqual(token.project_name, auth_ref.project_name) self.assertEqual(token.project_id, auth_ref.project_id) self.assertEqual(auth_ref.tenant_name, auth_ref.project_name) self.assertEqual(auth_ref.tenant_id, auth_ref.project_id) self.assertEqual(token.project_domain_id, auth_ref.project_domain_id) self.assertEqual(token.project_domain_name, auth_ref.project_domain_name) self.assertEqual(token.user_domain_id, auth_ref.user_domain_id) self.assertEqual(token.user_domain_name, auth_ref.user_domain_name) self.assertFalse(auth_ref.domain_scoped) self.assertTrue(auth_ref.project_scoped) self.assertTrue(auth_ref.project_is_domain) self.assertEqual(token.audit_id, auth_ref.audit_id) self.assertEqual(token.audit_chain_id, auth_ref.audit_chain_id) def test_oauth_access(self): consumer_id = uuid.uuid4().hex access_token_id = uuid.uuid4().hex token = fixture.V3Token() token.set_project_scope() token.set_oauth(access_token_id=access_token_id, consumer_id=consumer_id) auth_ref = access.create(body=token) self.assertEqual(consumer_id, auth_ref.oauth_consumer_id) self.assertEqual(access_token_id, auth_ref.oauth_access_token_id) self.assertEqual(consumer_id, auth_ref._data['token']['OS-OAUTH1']['consumer_id']) self.assertEqual( access_token_id, auth_ref._data['token']['OS-OAUTH1']['access_token_id']) def test_federated_property_standard_token(self): """Check if is_federated property returns expected value.""" token = fixture.V3Token() token.set_project_scope() auth_ref = access.create(body=token) self.assertFalse(auth_ref.is_federated) def test_binding(self): token = fixture.V3Token() principal = uuid.uuid4().hex token.set_bind('kerberos', principal) auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV3) self.assertEqual({'kerberos': principal}, auth_ref.bind) def test_is_admin_project_unset(self): token = fixture.V3Token() auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV3) self.assertIs(True, auth_ref.is_admin_project) def test_is_admin_project_true(self): token = fixture.V3Token(is_admin_project=True) auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV3) self.assertIs(True, auth_ref.is_admin_project) def test_is_admin_project_false(self): token = fixture.V3Token(is_admin_project=False) auth_ref = access.create(body=token) self.assertIsInstance(auth_ref, access.AccessInfoV3) self.assertIs(False, auth_ref.is_admin_project) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/access/test_v3_service_catalog.py0000664000175000017500000005073400000000000027720 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1.tests.unit import utils class ServiceCatalogTest(utils.TestCase): def setUp(self): super(ServiceCatalogTest, self).setUp() self.AUTH_RESPONSE_BODY = fixture.V3Token( audit_chain_id=uuid.uuid4().hex) self.AUTH_RESPONSE_BODY.set_project_scope() self.AUTH_RESPONSE_BODY.add_role(name='admin') self.AUTH_RESPONSE_BODY.add_role(name='member') s = self.AUTH_RESPONSE_BODY.add_service('compute', name='nova') s.add_standard_endpoints( public='https://compute.north.host/novapi/public', internal='https://compute.north.host/novapi/internal', admin='https://compute.north.host/novapi/admin', region='North') s = self.AUTH_RESPONSE_BODY.add_service('object-store', name='swift') s.add_standard_endpoints( public='http://swift.north.host/swiftapi/public', internal='http://swift.north.host/swiftapi/internal', admin='http://swift.north.host/swiftapi/admin', region='South') s = self.AUTH_RESPONSE_BODY.add_service('image', name='glance') s.add_standard_endpoints( public='http://glance.north.host/glanceapi/public', internal='http://glance.north.host/glanceapi/internal', admin='http://glance.north.host/glanceapi/admin', region='North') s.add_standard_endpoints( public='http://glance.south.host/glanceapi/public', internal='http://glance.south.host/glanceapi/internal', admin='http://glance.south.host/glanceapi/admin', region='South') s = self.AUTH_RESPONSE_BODY.add_service('block-storage', name='cinder') s.add_standard_endpoints( public='http://cinder.north.host/cinderapi/public', internal='http://cinder.north.host/cinderapi/internal', admin='http://cinder.north.host/cinderapi/admin', region='North') s = self.AUTH_RESPONSE_BODY.add_service('volumev2', name='cinder') s.add_standard_endpoints( public='http://cinder.south.host/cinderapi/public/v2', internal='http://cinder.south.host/cinderapi/internal/v2', admin='http://cinder.south.host/cinderapi/admin/v2', region='South') s = self.AUTH_RESPONSE_BODY.add_service('volumev3', name='cinder') s.add_standard_endpoints( public='http://cinder.south.host/cinderapi/public/v3', internal='http://cinder.south.host/cinderapi/internal/v3', admin='http://cinder.south.host/cinderapi/admin/v3', region='South') self.north_endpoints = {'public': 'http://glance.north.host/glanceapi/public', 'internal': 'http://glance.north.host/glanceapi/internal', 'admin': 'http://glance.north.host/glanceapi/admin'} self.south_endpoints = {'public': 'http://glance.south.host/glanceapi/public', 'internal': 'http://glance.south.host/glanceapi/internal', 'admin': 'http://glance.south.host/glanceapi/admin'} def test_building_a_service_catalog(self): auth_ref = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog self.assertEqual(sc.url_for(service_type='compute'), "https://compute.north.host/novapi/public") self.assertEqual(sc.url_for(service_type='compute', interface='internal'), "https://compute.north.host/novapi/internal") self.assertRaises(exceptions.EndpointNotFound, sc.url_for, region_name='South', service_type='compute') def test_service_catalog_endpoints(self): auth_ref = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog public_ep = sc.get_endpoints(service_type='compute', interface='public') self.assertEqual(public_ep['compute'][0]['region'], 'North') self.assertEqual(public_ep['compute'][0]['url'], "https://compute.north.host/novapi/public") def test_service_catalog_alias_find_official(self): auth_ref = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog # Tests that we find the block-storage endpoint when we request # the volume endpoint. public_ep = sc.get_endpoints(service_type='volume', interface='public', region_name='North') self.assertEqual(public_ep['block-storage'][0]['region'], 'North') self.assertEqual(public_ep['block-storage'][0]['url'], "http://cinder.north.host/cinderapi/public") def test_service_catalog_alias_find_exact_match(self): auth_ref = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog # Tests that we find the volumev3 endpoint when we request it. public_ep = sc.get_endpoints(service_type='volumev3', interface='public') self.assertEqual(public_ep['volumev3'][0]['region'], 'South') self.assertEqual(public_ep['volumev3'][0]['url'], "http://cinder.south.host/cinderapi/public/v3") def test_service_catalog_alias_find_best_match(self): auth_ref = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog # Tests that we find the volumev3 endpoint when we request # block-storage when only volumev2 and volumev3 are present since # volumev3 comes first in the list. public_ep = sc.get_endpoints(service_type='block-storage', interface='public', region_name='South') self.assertEqual(public_ep['volumev3'][0]['region'], 'South') self.assertEqual(public_ep['volumev3'][0]['url'], "http://cinder.south.host/cinderapi/public/v3") def test_service_catalog_alias_all_by_name(self): auth_ref = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog # Tests that we find all the cinder endpoints since we request # them by name and that no filtering related to aliases happens. public_ep = sc.get_endpoints(service_name='cinder', interface='public') self.assertEqual(public_ep['volumev2'][0]['region'], 'South') self.assertEqual(public_ep['volumev2'][0]['url'], "http://cinder.south.host/cinderapi/public/v2") self.assertEqual(public_ep['volumev3'][0]['region'], 'South') self.assertEqual(public_ep['volumev3'][0]['url'], "http://cinder.south.host/cinderapi/public/v3") self.assertEqual(public_ep['block-storage'][0]['region'], 'North') self.assertEqual(public_ep['block-storage'][0]['url'], "http://cinder.north.host/cinderapi/public") def test_service_catalog_regions(self): self.AUTH_RESPONSE_BODY['token']['region_name'] = "North" auth_ref = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog url = sc.url_for(service_type='image', interface='public') self.assertEqual(url, "http://glance.north.host/glanceapi/public") self.AUTH_RESPONSE_BODY['token']['region_name'] = "South" auth_ref = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY) sc = auth_ref.service_catalog url = sc.url_for(service_type='image', region_name="South", interface='internal') self.assertEqual(url, "http://glance.south.host/glanceapi/internal") def test_service_catalog_empty(self): self.AUTH_RESPONSE_BODY['token']['catalog'] = [] auth_ref = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY) self.assertRaises(exceptions.EmptyCatalog, auth_ref.service_catalog.url_for, service_type='image', interface='internalURL') def test_service_catalog_get_endpoints_region_names(self): sc = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY).service_catalog endpoints = sc.get_endpoints(service_type='image', region_name='North') self.assertEqual(len(endpoints), 1) for endpoint in endpoints['image']: self.assertEqual(endpoint['url'], self.north_endpoints[endpoint['interface']]) endpoints = sc.get_endpoints(service_type='image', region_name='South') self.assertEqual(len(endpoints), 1) for endpoint in endpoints['image']: self.assertEqual(endpoint['url'], self.south_endpoints[endpoint['interface']]) endpoints = sc.get_endpoints(service_type='compute') self.assertEqual(len(endpoints['compute']), 3) endpoints = sc.get_endpoints(service_type='compute', region_name='North') self.assertEqual(len(endpoints['compute']), 3) endpoints = sc.get_endpoints(service_type='compute', region_name='West') self.assertEqual(len(endpoints['compute']), 0) def test_service_catalog_url_for_region_names(self): sc = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY).service_catalog url = sc.url_for(service_type='image', region_name='North') self.assertEqual(url, self.north_endpoints['public']) url = sc.url_for(service_type='image', region_name='South') self.assertEqual(url, self.south_endpoints['public']) self.assertRaises(exceptions.EndpointNotFound, sc.url_for, service_type='image', region_name='West') def test_service_catalog_get_url_region_names(self): sc = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY).service_catalog urls = sc.get_urls(service_type='image') self.assertEqual(len(urls), 2) urls = sc.get_urls(service_type='image', region_name='North') self.assertEqual(len(urls), 1) self.assertEqual(urls[0], self.north_endpoints['public']) urls = sc.get_urls(service_type='image', region_name='South') self.assertEqual(len(urls), 1) self.assertEqual(urls[0], self.south_endpoints['public']) urls = sc.get_urls(service_type='image', region_name='West') self.assertEqual(len(urls), 0) def test_service_catalog_service_name(self): sc = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY).service_catalog url = sc.url_for(service_name='glance', interface='public', service_type='image', region_name='North') self.assertEqual('http://glance.north.host/glanceapi/public', url) url = sc.url_for(service_name='glance', interface='public', service_type='image', region_name='South') self.assertEqual('http://glance.south.host/glanceapi/public', url) self.assertRaises(exceptions.EndpointNotFound, sc.url_for, service_name='glance', service_type='compute') urls = sc.get_urls(service_type='image', service_name='glance', interface='public') self.assertIn('http://glance.north.host/glanceapi/public', urls) self.assertIn('http://glance.south.host/glanceapi/public', urls) urls = sc.get_urls(service_type='image', service_name='Servers', interface='public') self.assertEqual(0, len(urls)) def test_service_catalog_without_name(self): f = fixture.V3Token(audit_chain_id=uuid.uuid4().hex) if not f.project_id: f.set_project_scope() f.add_role(name='admin') f.add_role(name='member') region = 'RegionOne' tenant = '225da22d3ce34b15877ea70b2a575f58' s = f.add_service('volume') s.add_standard_endpoints( public='http://public.com:8776/v1/%s' % tenant, internal='http://internal:8776/v1/%s' % tenant, admin='http://admin:8776/v1/%s' % tenant, region=region) s = f.add_service('image') s.add_standard_endpoints(public='http://public.com:9292/v1', internal='http://internal:9292/v1', admin='http://admin:9292/v1', region=region) s = f.add_service('compute') s.add_standard_endpoints( public='http://public.com:8774/v2/%s' % tenant, internal='http://internal:8774/v2/%s' % tenant, admin='http://admin:8774/v2/%s' % tenant, region=region) s = f.add_service('ec2') s.add_standard_endpoints( public='http://public.com:8773/services/Cloud', internal='http://internal:8773/services/Cloud', admin='http://admin:8773/services/Admin', region=region) s = f.add_service('identity') s.add_standard_endpoints(public='http://public.com:5000/v3', internal='http://internal:5000/v3', admin='http://admin:35357/v3', region=region) pr_auth_ref = access.create(body=f) pr_sc = pr_auth_ref.service_catalog # this will work because there are no service names on that token url_ref = 'http://public.com:8774/v2/225da22d3ce34b15877ea70b2a575f58' url = pr_sc.url_for(service_type='compute', service_name='NotExist', interface='public') self.assertEqual(url_ref, url) ab_auth_ref = access.create(body=self.AUTH_RESPONSE_BODY) ab_sc = ab_auth_ref.service_catalog # this won't work because there is a name and it's not this one self.assertRaises(exceptions.EndpointNotFound, ab_sc.url_for, service_type='compute', service_name='NotExist', interface='public') class ServiceCatalogV3Test(ServiceCatalogTest): def test_building_a_service_catalog(self): sc = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY).service_catalog self.assertEqual(sc.url_for(service_type='compute'), 'https://compute.north.host/novapi/public') self.assertEqual(sc.url_for(service_type='compute', interface='internal'), 'https://compute.north.host/novapi/internal') self.assertRaises(exceptions.EndpointNotFound, sc.url_for, region_name='South', service_type='compute') def test_service_catalog_endpoints(self): sc = access.create(auth_token=uuid.uuid4().hex, body=self.AUTH_RESPONSE_BODY).service_catalog public_ep = sc.get_endpoints(service_type='compute', interface='public') self.assertEqual(public_ep['compute'][0]['region_id'], 'North') self.assertEqual(public_ep['compute'][0]['url'], 'https://compute.north.host/novapi/public') def test_service_catalog_multiple_service_types(self): token = fixture.V3Token() token.set_project_scope() for i in range(3): s = token.add_service('compute') s.add_standard_endpoints(public='public-%d' % i, admin='admin-%d' % i, internal='internal-%d' % i, region='region-%d' % i) auth_ref = access.create(resp=None, body=token) urls = auth_ref.service_catalog.get_urls(service_type='compute', interface='public') self.assertEqual(set(['public-0', 'public-1', 'public-2']), set(urls)) urls = auth_ref.service_catalog.get_urls(service_type='compute', interface='public', region_name='region-1') self.assertEqual(('public-1', ), urls) def test_service_catalog_endpoint_id(self): token = fixture.V3Token() token.set_project_scope() service_id = uuid.uuid4().hex endpoint_id = uuid.uuid4().hex public_url = uuid.uuid4().hex s = token.add_service('compute', id=service_id) s.add_endpoint('public', public_url, id=endpoint_id) s.add_endpoint('public', uuid.uuid4().hex) auth_ref = access.create(body=token) # initially assert that we get back all our urls for a simple filter urls = auth_ref.service_catalog.get_urls(service_type='compute', interface='public') self.assertEqual(2, len(urls)) # with bad endpoint_id nothing should be found urls = auth_ref.service_catalog.get_urls(service_type='compute', endpoint_id=uuid.uuid4().hex, interface='public') self.assertEqual(0, len(urls)) # with service_id we get back both public endpoints urls = auth_ref.service_catalog.get_urls(service_type='compute', service_id=service_id, interface='public') self.assertEqual(2, len(urls)) # with service_id and endpoint_id we get back the url we want urls = auth_ref.service_catalog.get_urls(service_type='compute', service_id=service_id, endpoint_id=endpoint_id, interface='public') self.assertEqual((public_url, ), urls) # with service_id and endpoint_id we get back the url we want urls = auth_ref.service_catalog.get_urls(service_type='compute', endpoint_id=endpoint_id, interface='public') self.assertEqual((public_url, ), urls) def test_service_catalog_without_service_type(self): token = fixture.V3Token() token.set_project_scope() public_urls = [] for i in range(0, 3): public_url = uuid.uuid4().hex public_urls.append(public_url) s = token.add_service(uuid.uuid4().hex) s.add_endpoint('public', public_url) auth_ref = access.create(body=token) urls = auth_ref.service_catalog.get_urls(interface='public') self.assertEqual(3, len(urls)) for p in public_urls: self.assertIn(p, urls) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/client_fixtures.py0000664000175000017500000001136600000000000025063 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import fixture as kfixture def project_scoped_token(): fixture = kfixture.V3Token( user_id='c4da488862bd435c9e6c0275a0d0e49a', user_name='exampleuser', user_domain_id='4e6893b7ba0b4006840c3845660b86ed', user_domain_name='exampledomain', expires='2010-11-01T03:32:15-05:00', project_id='225da22d3ce34b15877ea70b2a575f58', project_name='exampleproject', project_domain_id='4e6893b7ba0b4006840c3845660b86ed', project_domain_name='exampledomain') fixture.add_role(id='76e72a', name='admin') fixture.add_role(id='f4f392', name='member') region = 'RegionOne' tenant = '225da22d3ce34b15877ea70b2a575f58' service = fixture.add_service('volume') service.add_standard_endpoints( public='http://public.com:8776/v1/%s' % tenant, internal='http://internal:8776/v1/%s' % tenant, admin='http://admin:8776/v1/%s' % tenant, region=region) service = fixture.add_service('image') service.add_standard_endpoints(public='http://public.com:9292/v1', internal='http://internal:9292/v1', admin='http://admin:9292/v1', region=region) service = fixture.add_service('compute') service.add_standard_endpoints( public='http://public.com:8774/v2/%s' % tenant, internal='http://internal:8774/v2/%s' % tenant, admin='http://admin:8774/v2/%s' % tenant, region=region) service = fixture.add_service('ec2') service.add_standard_endpoints( public='http://public.com:8773/services/Cloud', internal='http://internal:8773/services/Cloud', admin='http://admin:8773/services/Admin', region=region) service = fixture.add_service('identity') service.add_standard_endpoints(public='http://public.com:5000/v3', internal='http://internal:5000/v3', admin='http://admin:35357/v3', region=region) return fixture def domain_scoped_token(): fixture = kfixture.V3Token( user_id='c4da488862bd435c9e6c0275a0d0e49a', user_name='exampleuser', user_domain_id='4e6893b7ba0b4006840c3845660b86ed', user_domain_name='exampledomain', expires='2010-11-01T03:32:15-05:00', domain_id='8e9283b7ba0b1038840c3842058b86ab', domain_name='anotherdomain') fixture.add_role(id='76e72a', name='admin') fixture.add_role(id='f4f392', name='member') region = 'RegionOne' service = fixture.add_service('volume') service.add_standard_endpoints(public='http://public.com:8776/v1/None', internal='http://internal.com:8776/v1/None', admin='http://admin.com:8776/v1/None', region=region) service = fixture.add_service('image') service.add_standard_endpoints(public='http://public.com:9292/v1', internal='http://internal:9292/v1', admin='http://admin:9292/v1', region=region) service = fixture.add_service('compute') service.add_standard_endpoints(public='http://public.com:8774/v1.1/None', internal='http://internal:8774/v1.1/None', admin='http://admin:8774/v1.1/None', region=region) service = fixture.add_service('ec2') service.add_standard_endpoints( public='http://public.com:8773/services/Cloud', internal='http://internal:8773/services/Cloud', admin='http://admin:8773/services/Admin', region=region) service = fixture.add_service('identity') service.add_standard_endpoints(public='http://public.com:5000/v3', internal='http://internal:5000/v3', admin='http://admin:35357/v3', region=region) return fixture AUTH_SUBJECT_TOKEN = '3e2813b7ba0b4006840c3825860b86ed' AUTH_RESPONSE_HEADERS = { 'X-Subject-Token': AUTH_SUBJECT_TOKEN, } ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3047943 keystoneauth1-4.4.0/keystoneauth1/tests/unit/data/0000775000175000017500000000000000000000000022204 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/data/README0000664000175000017500000000052100000000000023062 0ustar00zuulzuul00000000000000This directory holds the betamax test cassettes that are pre-generated for unit testing. This can be removed in the future with a functional test that stands up a full devstack, records a cassette and then replays it as part of the test suite. Until the functional testing is implemented do not remove this directory or enclosed files. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/data/keystone_v2_sample_request.json0000664000175000017500000000015500000000000030461 0ustar00zuulzuul00000000000000{"auth":{"tenantName": "customer-x", "passwordCredentials": {"username": "joeuser", "password": "secrete"}}} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/data/keystone_v2_sample_response.json0000664000175000017500000000255100000000000030631 0ustar00zuulzuul00000000000000{ "access":{ "token":{ "expires":"2012-02-05T00:00:00", "id":"887665443383838", "tenant":{ "id":"1", "name":"customer-x" } }, "serviceCatalog":[ { "endpoints":[ { "adminURL":"http://swift.admin-nets.local:8080/", "region":"RegionOne", "internalURL":"http://127.0.0.1:8080/v1/AUTH_1", "publicURL":"http://swift.publicinternets.com/v1/AUTH_1" } ], "type":"object-store", "name":"swift" }, { "endpoints":[ { "adminURL":"http://cdn.admin-nets.local/v1.1/1", "region":"RegionOne", "internalURL":"http://127.0.0.1:7777/v1.1/1", "publicURL":"http://cdn.publicinternets.com/v1.1/1" } ], "type":"object-store", "name":"cdn" } ], "user":{ "id":"1", "roles":[ { "tenantId":"1", "id":"3", "name":"Member" } ], "name":"joeuser" } } } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/data/keystone_v3_sample_request.json0000664000175000017500000000034600000000000030464 0ustar00zuulzuul00000000000000{ "auth": { "identity": { "methods": ["password"], "password": { "user": { "name": "admin", "domain": { "id": "default" }, "password": "adminpwd" } } } } } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/data/keystone_v3_sample_response.json0000664000175000017500000000202400000000000030625 0ustar00zuulzuul00000000000000{"token": {"methods": ["password"], "roles": [{"id": "9fe2ff9ee4384b1894a90878d3e92bab", "name": "_member_"}, {"id": "c703057be878458588961ce9a0ce686b", "name": "admin"}], "expires_at": "2014-06-10T2:55:16.806001Z", "project": {"domain": {"id": "default", "name": "Default"}, "id": "8538a3f13f9541b28c2620eb19065e45", "name": "admin"}, "catalog": [{"endpoints": [{"url": "http://localhost:3537/v2.0", "region": "RegionOne", "interface": "admin", "id": "29beb2f1567642eb810b042b6719ea88"}, {"url": "http://localhost:5000/v2.0", "region": "RegionOne", "interface": "internal", "id": "8707e3735d4415c97ae231b4841eb1c"}, {"url": "http://localhost:5000/v2.0", "region": "RegionOne", "interface": "public", "id": "ef303187fc8d41668f25199c298396a5"}], "type": "identity", "id": "bd73972c0e14fb69bae8ff76e112a90", "name": "keystone"}], "extras": {}, "user": {"domain": {"id": "default", "name": "Default"}, "id": "3ec3164f750146be97f21559ee4d9c51", "name": "admin"}, "audit_ids": ["yRt0UrxJSs6-WYJgwEMMmg"], "issued_at": "201406-10T20:55:16.806027Z"}} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/data/ksa_betamax_test_cassette.yaml0000664000175000017500000000435600000000000030311 0ustar00zuulzuul00000000000000http_interactions: - request: body: string: |- { "auth": { "tenantName": "test_tenant_name", "passwordCredentials": { "username": "test_user_name", "password": "test_password" } } } encoding: utf-8 headers: Content-Length: - '128' Accept-Encoding: - gzip, deflate Accept: - application/json User-Agent: - keystoneauth1 Connection: - keep-alive Content-Type: - application/json method: POST uri: http://keystoneauth-betamax.test/v2.0/tokens response: body: string: |- { "access": { "token": { "issued_at": "2015-11-27T15:17:19.755470", "expires": "2015-11-27T16:17:19Z", "id": "c000c5ee4ba04594a00886028584b50d", "tenant": { "enabled": true, "description": null, "name": "test_tenant_name", "id": "6932cad596634a61ac9c759fb91beef1" }, "audit_ids": [ "jY3gYg_YTbmzY2a4ioGuCw" ] }, "user": { "username": "test_user_name", "roles_links": [], "id": "96995e6cc15b40fa8e7cd762f6a5d4c0", "roles": [ { "name": "_member_" } ], "name": "67eff5f6-9477-4961-88b4-437e6596a795" }, "metadata": { "is_admin": 0, "roles": [ "9fe2ff9ee4384b1894a90878d3e92bab" ] } } } encoding: null headers: X-Openstack-Request-Id: - req-f9e188b4-06fd-4a4c-a952-2315b368218c Content-Length: - '2684' Connection: - keep-alive Date: - Fri, 27 Nov 2015 15:17:19 GMT Content-Type: - application/json Vary: - X-Auth-Token X-Distribution: - Ubuntu Server: - Fake status: message: OK code: 200 url: http://keystoneauth-betamax.test/v2.0/tokens recorded_at: '2015-11-27T15:17:19' recorded_with: betamax/0.5.1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/data/ksa_serializer_data.json0000664000175000017500000000317700000000000027107 0ustar00zuulzuul00000000000000{"http_interactions": [{"request": {"body": {"string": "{\"auth\": {\"tenantName\": \"test_tenant_name\", \"passwordCredentials\": {\"username\": \"test_user_name\", \"password\": \"test_password\"}}}", "encoding": "utf-8"}, "headers": {"Content-Length": ["128"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["application/json"], "User-Agent": ["keystoneauth1"], "Connection": ["keep-alive"], "Content-Type": ["application/json"]}, "method": "POST", "uri": "http://keystoneauth-betamax.test/v2.0/tokens"}, "response": {"body": {"string": "{\"access\": {\"token\": {\"issued_at\": \"2015-11-27T15:17:19.755470\", \"expires\": \"2015-11-27T16:17:19Z\", \"id\": \"c000c5ee4ba04594a00886028584b50d\", \"tenant\": {\"description\": null, \"enabled\": true, \"id\": \"6932cad596634a61ac9c759fb91beef1\", \"name\": \"test_tenant_name\"}, \"audit_ids\": [\"jY3gYg_YTbmzY2a4ioGuCw\"]}, \"user\": {\"username\": \"test_user_name\", \"roles_links\": [], \"id\": \"96995e6cc15b40fa8e7cd762f6a5d4c0\", \"roles\": [{\"name\": \"_member_\"}], \"name\": \"67eff5f6-9477-4961-88b4-437e6596a795\"}, \"metadata\": {\"is_admin\": 0, \"roles\": [\"9fe2ff9ee4384b1894a90878d3e92bab\"]}}}", "encoding": null}, "headers": {"X-Openstack-Request-Id": ["req-f9e188b4-06fd-4a4c-a952-2315b368218c"], "Content-Length": ["2684"], "Connection": ["keep-alive"], "Date": ["Fri, 27 Nov 2015 15:17:19 GMT"], "Content-Type": ["application/json"], "Vary": ["X-Auth-Token"], "X-Distribution": ["Ubuntu"], "Server": ["Fake"]}, "status": {"message": "OK", "code": 200}, "url": "http://keystoneauth-betamax.test/v2.0/tokens"}, "recorded_at": "2015-11-27T15:17:19"}], "recorded_with": "betamax/0.5.1"} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/data/test_pre_record_hook.json0000664000175000017500000000000000000000000027270 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3047943 keystoneauth1-4.4.0/keystoneauth1/tests/unit/exceptions/0000775000175000017500000000000000000000000023454 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/exceptions/__init__.py0000664000175000017500000000000000000000000025553 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/exceptions/test_exceptions.py0000664000175000017500000000237300000000000027253 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import exceptions from keystoneauth1.tests.unit import utils class ExceptionTests(utils.TestCase): def test_clientexception_with_message(self): test_message = 'Unittest exception message.' exc = exceptions.ClientException(message=test_message) self.assertEqual(test_message, exc.message) def test_clientexception_with_no_message(self): exc = exceptions.ClientException() self.assertEqual(exceptions.ClientException.__name__, exc.message) def test_using_default_message(self): exc = exceptions.AuthorizationFailure() self.assertEqual(exceptions.AuthorizationFailure.message, exc.message) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3047943 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/0000775000175000017500000000000000000000000022601 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/__init__.py0000664000175000017500000000000000000000000024700 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3087943 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/kerberos/0000775000175000017500000000000000000000000024415 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/kerberos/__init__.py0000664000175000017500000000000000000000000026514 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/kerberos/base.py0000664000175000017500000000304700000000000025705 0ustar00zuulzuul00000000000000# Copyright 2010-2011 OpenStack Foundation # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.tests.unit.extras.kerberos import utils from keystoneauth1.tests.unit import utils as test_utils REQUEST = {'auth': {'identity': {'methods': ['kerberos'], 'kerberos': {}}}} class TestCase(test_utils.TestCase): """Test case base class for Kerberos unit tests.""" TEST_V3_URL = test_utils.TestCase.TEST_ROOT_URL + 'v3' def setUp(self): super(TestCase, self).setUp() km = utils.KerberosMock(self.requests_mock) self.kerberos_mock = self.useFixture(km) def assertRequestBody(self, body=None): """Ensure the request body is the standard Kerberos auth request. :param dict body: the body to compare. If not provided the last request body will be used. """ if not body: body = self.requests_mock.last_request.json() self.assertEqual(REQUEST, body) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/kerberos/test_fedkerb_loading.py0000664000175000017500000000342000000000000031124 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import exceptions from keystoneauth1 import loading from keystoneauth1.tests.unit import utils as test_utils class FedKerbLoadingTests(test_utils.TestCase): def test_options(self): opts = [o.name for o in loading.get_plugin_loader('v3fedkerb').get_options()] allowed_opts = ['system-scope', 'domain-id', 'domain-name', 'identity-provider', 'project-id', 'project-name', 'project-domain-id', 'project-domain-name', 'protocol', 'trust-id', 'auth-url', 'mutual-auth', ] self.assertCountEqual(allowed_opts, opts) def create(self, **kwargs): loader = loading.get_plugin_loader('v3fedkerb') return loader.load_from_options(**kwargs) def test_load_none(self): self.assertRaises(exceptions.MissingRequiredOptions, self.create) def test_load(self): self.create(auth_url='auth_url', identity_provider='idp', protocol='protocol') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/kerberos/test_kerberos_loading.py0000664000175000017500000000241700000000000031343 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import loading from keystoneauth1.tests.unit import utils as test_utils class KerberosLoadingTests(test_utils.TestCase): def test_options(self): opts = [o.name for o in loading.get_plugin_loader('v3kerberos').get_options()] allowed_opts = ['system-scope', 'domain-id', 'domain-name', 'project-id', 'project-name', 'project-domain-id', 'project-domain-name', 'trust-id', 'auth-url', 'mutual-auth', ] self.assertCountEqual(allowed_opts, opts) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/kerberos/test_mapped.py0000664000175000017500000001122200000000000027272 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from keystoneauth1.extras import kerberos from keystoneauth1 import fixture as ks_fixture from keystoneauth1 import session from keystoneauth1.tests.unit.extras.kerberos import base class TestMappedAuth(base.TestCase): def setUp(self): if kerberos.requests_kerberos is None: self.skipTest("Kerberos support isn't available.") super(TestMappedAuth, self).setUp() self.protocol = uuid.uuid4().hex self.identity_provider = uuid.uuid4().hex @property def token_url(self): fmt = '%s/OS-FEDERATION/identity_providers/%s/protocols/%s/auth' return fmt % ( self.TEST_V3_URL, self.identity_provider, self.protocol) def test_unscoped_mapped_auth(self): token_id, _ = self.kerberos_mock.mock_auth_success( url=self.token_url, method='GET') plugin = kerberos.MappedKerberos( auth_url=self.TEST_V3_URL, protocol=self.protocol, identity_provider=self.identity_provider) sess = session.Session() tok = plugin.get_token(sess) self.assertEqual(token_id, tok) def test_project_scoped_mapped_auth(self): self.kerberos_mock.mock_auth_success(url=self.token_url, method='GET') scoped_id = uuid.uuid4().hex scoped_body = ks_fixture.V3Token() scoped_body.set_project_scope() self.requests_mock.post( '%s/auth/tokens' % self.TEST_V3_URL, json=scoped_body, headers={'X-Subject-Token': scoped_id, 'Content-Type': 'application/json'}) plugin = kerberos.MappedKerberos( auth_url=self.TEST_V3_URL, protocol=self.protocol, identity_provider=self.identity_provider, project_id=scoped_body.project_id) sess = session.Session() tok = plugin.get_token(sess) proj = plugin.get_project_id(sess) self.assertEqual(scoped_id, tok) self.assertEqual(scoped_body.project_id, proj) def test_authenticate_with_mutual_authentication_required(self): self.kerberos_mock.mock_auth_success(url=self.token_url, method='GET') scoped_id = uuid.uuid4().hex scoped_body = ks_fixture.V3Token() scoped_body.set_project_scope() self.requests_mock.post( '%s/auth/tokens' % self.TEST_V3_URL, json=scoped_body, headers={'X-Subject-Token': scoped_id, 'Content-Type': 'application/json'}) plugin = kerberos.MappedKerberos( auth_url=self.TEST_V3_URL, protocol=self.protocol, identity_provider=self.identity_provider, project_id=scoped_body.project_id, mutual_auth='required') sess = session.Session() tok = plugin.get_token(sess) proj = plugin.get_project_id(sess) self.assertEqual(scoped_id, tok) self.assertEqual(scoped_body.project_id, proj) self.assertEqual(self.kerberos_mock.called_auth_server, True) def test_authenticate_with_mutual_authentication_disabled(self): self.kerberos_mock.mock_auth_success(url=self.token_url, method='GET') scoped_id = uuid.uuid4().hex scoped_body = ks_fixture.V3Token() scoped_body.set_project_scope() self.requests_mock.post( '%s/auth/tokens' % self.TEST_V3_URL, json=scoped_body, headers={'X-Subject-Token': scoped_id, 'Content-Type': 'application/json'}) plugin = kerberos.MappedKerberos( auth_url=self.TEST_V3_URL, protocol=self.protocol, identity_provider=self.identity_provider, project_id=scoped_body.project_id, mutual_auth='disabled') sess = session.Session() tok = plugin.get_token(sess) proj = plugin.get_project_id(sess) self.assertEqual(scoped_id, tok) self.assertEqual(scoped_body.project_id, proj) self.assertEqual(self.kerberos_mock.called_auth_server, False) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/kerberos/test_v3.py0000664000175000017500000000533000000000000026357 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.extras import kerberos from keystoneauth1 import session from keystoneauth1.tests.unit.extras.kerberos import base class TestKerberosAuth(base.TestCase): def setUp(self): if kerberos.requests_kerberos is None: self.skipTest("Kerberos support isn't available.") super(TestKerberosAuth, self).setUp() def test_authenticate_with_kerberos_domain_scoped(self): token_id, token_body = self.kerberos_mock.mock_auth_success() a = kerberos.Kerberos(self.TEST_ROOT_URL + 'v3') s = session.Session(a) token = a.get_token(s) self.assertRequestBody() self.assertEqual( self.kerberos_mock.challenge_header, self.requests_mock.last_request.headers['Authorization']) self.assertEqual(token_id, a.auth_ref.auth_token) self.assertEqual(token_id, token) def test_authenticate_with_kerberos_mutual_authentication_required(self): token_id, token_body = self.kerberos_mock.mock_auth_success() a = kerberos.Kerberos(self.TEST_ROOT_URL + 'v3', mutual_auth='required') s = session.Session(a) token = a.get_token(s) self.assertRequestBody() self.assertEqual( self.kerberos_mock.challenge_header, self.requests_mock.last_request.headers['Authorization']) self.assertEqual(token_id, a.auth_ref.auth_token) self.assertEqual(token_id, token) self.assertEqual(self.kerberos_mock.called_auth_server, True) def test_authenticate_with_kerberos_mutual_authentication_disabled(self): token_id, token_body = self.kerberos_mock.mock_auth_success() a = kerberos.Kerberos(self.TEST_ROOT_URL + 'v3', mutual_auth='disabled') s = session.Session(a) token = a.get_token(s) self.assertRequestBody() self.assertEqual( self.kerberos_mock.challenge_header, self.requests_mock.last_request.headers['Authorization']) self.assertEqual(token_id, a.auth_ref.auth_token) self.assertEqual(token_id, token) self.assertEqual(self.kerberos_mock.called_auth_server, False) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/kerberos/utils.py0000664000175000017500000000600600000000000026131 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid import fixtures try: # requests_kerberos won't be available on py3, it doesn't work with py3. import requests_kerberos except ImportError: requests_kerberos = None from keystoneauth1 import fixture as ks_fixture from keystoneauth1.tests.unit import utils as test_utils class KerberosMock(fixtures.Fixture): def __init__(self, requests_mock): super(KerberosMock, self).__init__() self.challenge_header = 'Negotiate %s' % uuid.uuid4().hex self.pass_header = 'Negotiate %s' % uuid.uuid4().hex self.requests_mock = requests_mock def setUp(self): super(KerberosMock, self).setUp() if requests_kerberos is None: return m = fixtures.MockPatchObject(requests_kerberos.HTTPKerberosAuth, 'generate_request_header', self._generate_request_header) self.header_fixture = self.useFixture(m) m = fixtures.MockPatchObject(requests_kerberos.HTTPKerberosAuth, 'authenticate_server', self._authenticate_server) self.authenticate_fixture = self.useFixture(m) def _generate_request_header(self, *args, **kwargs): return self.challenge_header def _authenticate_server(self, response): self.called_auth_server = True return response.headers.get('www-authenticate') == self.pass_header def mock_auth_success( self, token_id=None, token_body=None, method='POST', url=test_utils.TestCase.TEST_ROOT_URL + 'v3/auth/tokens'): if not token_id: token_id = uuid.uuid4().hex if not token_body: token_body = ks_fixture.V3Token() self.called_auth_server = False response_list = [{'text': 'Fail', 'status_code': 401, 'headers': {'WWW-Authenticate': 'Negotiate'}}, {'headers': {'X-Subject-Token': token_id, 'Content-Type': 'application/json', 'WWW-Authenticate': self.pass_header}, 'status_code': 200, 'json': token_body}] self.requests_mock.register_uri(method, url, response_list=response_list) return token_id, token_body ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3087943 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/oauth1/0000775000175000017500000000000000000000000024002 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/oauth1/__init__.py0000664000175000017500000000000000000000000026101 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/oauth1/test_oauth1.py0000664000175000017500000001102100000000000026607 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from oauthlib import oauth1 import six from testtools import matchers from keystoneauth1.extras import oauth1 as ksa_oauth1 from keystoneauth1 import fixture from keystoneauth1 import session from keystoneauth1.tests.unit import utils as test_utils class OAuth1AuthTests(test_utils.TestCase): TEST_ROOT_URL = 'http://127.0.0.1:5000/' TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') TEST_TOKEN = uuid.uuid4().hex def stub_auth(self, subject_token=None, **kwargs): if not subject_token: subject_token = self.TEST_TOKEN self.stub_url('POST', ['auth', 'tokens'], headers={'X-Subject-Token': subject_token}, **kwargs) def _validate_oauth_headers(self, auth_header, oauth_client): """Validate data in the headers. Assert that the data in the headers matches the data that is produced from oauthlib. """ self.assertThat(auth_header, matchers.StartsWith('OAuth ')) parameters = dict( oauth1.rfc5849.utils.parse_authorization_header(auth_header)) self.assertEqual('HMAC-SHA1', parameters['oauth_signature_method']) self.assertEqual('1.0', parameters['oauth_version']) self.assertIsInstance(parameters['oauth_nonce'], six.string_types) self.assertEqual(oauth_client.client_key, parameters['oauth_consumer_key']) if oauth_client.resource_owner_key: self.assertEqual(oauth_client.resource_owner_key, parameters['oauth_token'],) if oauth_client.verifier: self.assertEqual(oauth_client.verifier, parameters['oauth_verifier']) if oauth_client.callback_uri: self.assertEqual(oauth_client.callback_uri, parameters['oauth_callback']) return parameters def test_oauth_authenticate_success(self): consumer_key = uuid.uuid4().hex consumer_secret = uuid.uuid4().hex access_key = uuid.uuid4().hex access_secret = uuid.uuid4().hex oauth_token = fixture.V3Token(methods=['oauth1'], oauth_consumer_id=consumer_key, oauth_access_token_id=access_key) oauth_token.set_project_scope() self.stub_auth(json=oauth_token) a = ksa_oauth1.V3OAuth1(self.TEST_URL, consumer_key=consumer_key, consumer_secret=consumer_secret, access_key=access_key, access_secret=access_secret) s = session.Session(auth=a) t = s.get_token() self.assertEqual(self.TEST_TOKEN, t) OAUTH_REQUEST_BODY = { "auth": { "identity": { "methods": ["oauth1"], "oauth1": {} } } } self.assertRequestBodyIs(json=OAUTH_REQUEST_BODY) # Assert that the headers have the same oauthlib data req_headers = self.requests_mock.last_request.headers oauth_client = oauth1.Client(consumer_key, client_secret=consumer_secret, resource_owner_key=access_key, resource_owner_secret=access_secret, signature_method=oauth1.SIGNATURE_HMAC) self._validate_oauth_headers(req_headers['Authorization'], oauth_client) def test_warning_dual_scope(self): ksa_oauth1.V3OAuth1(self.TEST_URL, consumer_key=uuid.uuid4().hex, consumer_secret=uuid.uuid4().hex, access_key=uuid.uuid4().hex, access_secret=uuid.uuid4().hex, project_id=uuid.uuid4().hex) self.assertIn('ignored by the identity server', self.logger.output) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/oauth1/test_oauth1_loading.py0000664000175000017500000000414500000000000030315 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from keystoneauth1 import loading from keystoneauth1.tests.unit import utils as test_utils class OAuth1LoadingTests(test_utils.TestCase): def setUp(self): super(OAuth1LoadingTests, self).setUp() self.auth_url = uuid.uuid4().hex def create(self, **kwargs): kwargs.setdefault('auth_url', self.auth_url) loader = loading.get_plugin_loader('v3oauth1') return loader.load_from_options(**kwargs) def test_basic(self): access_key = uuid.uuid4().hex access_secret = uuid.uuid4().hex consumer_key = uuid.uuid4().hex consumer_secret = uuid.uuid4().hex p = self.create(access_key=access_key, access_secret=access_secret, consumer_key=consumer_key, consumer_secret=consumer_secret) oauth_method = p.auth_methods[0] self.assertEqual(self.auth_url, p.auth_url) self.assertEqual(access_key, oauth_method.access_key) self.assertEqual(access_secret, oauth_method.access_secret) self.assertEqual(consumer_key, oauth_method.consumer_key) self.assertEqual(consumer_secret, oauth_method.consumer_secret) def test_options(self): options = loading.get_plugin_loader('v3oauth1').get_options() self.assertEqual(set([o.name for o in options]), set(['auth-url', 'access-key', 'access-secret', 'consumer-key', 'consumer-secret'])) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3087943 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/0000775000175000017500000000000000000000000023617 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/__init__.py0000664000175000017500000000000000000000000025716 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2647943 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/examples/0000775000175000017500000000000000000000000025435 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3087943 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/examples/xml/0000775000175000017500000000000000000000000026235 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000021400000000000011452 xustar0000000000000000118 path=keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/examples/xml/ADFS_RequestSecurityTokenResponse.xml 22 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/examples/xml/ADFS_RequestSecurityTokenResp0000664000175000017500000003347200000000000034001 0ustar00zuulzuul00000000000000 http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal urn:uuid:487c064b-b7c6-4654-b4d4-715f9961170e 2014-08-05T18:36:14.235Z 2014-08-05T18:41:14.235Z 2014-08-05T18:36:14.063Z 2014-08-05T19:36:14.063Z https://ltartari2.cern.ch:5000/Shibboleth.sso/ADFS https://ltartari2.cern.ch:5000/Shibboleth.sso/ADFS marek.denis@cern.ch urn:oasis:names:tc:SAML:1.0:cm:bearer marek.denis@cern.ch marek.denis@cern.ch madenis CERN Users Domain Users occupants-bldg-31 CERN-Direct-Employees ca-dev-allowed cernts-cerntstest-users staf-fell-pjas-at-cern ELG-CERN student-club-new-members pawel-dynamic-test-82 Marek Kamil Denis +5555555 31S-013 Marek Kamil Denis CERN Registered CERN Normal marek.denis@cern.ch urn:oasis:names:tc:SAML:1.0:cm:bearer EaZ/2d0KAY5un9akV3++Npyk6hBc8JuTYs2S3lSxUeQ= CxYiYvNsbedhHdmDbb9YQCBy6Ppus3bNJdw2g2HLq0VU2yRhv23mUW05I89Hs4yG4OcCo0uOZ3zaeNFbSNXMW+Mr996tAXtujKjgyrCXNJAToE+gwltvGxwY1EluSbe3IzoSM3Ao87mKhxGOSzlDhuN7dQ9Rv6l/J4gUjbOO5SIX4pdZ6mVF7cHEfe9x+H8Lg15YjnElQUEaPi+NSW5jYTdtIpsB4ORxJvALuSt6+4doDYc9wuwBiWkEdnBHAQBINoKpAV2oy0/C85SBX3IdRhxUznmL5yEUmf8JvPccXecMPqJow0L43mnCdu74xPwU0as3MNfYQ10kLvHXHfIExg== MIIIEjCCBfqgAwIBAgIKLYgjvQAAAAAAMDANBgkqhkiG9w0BAQsFADBRMRIwEAYKCZImiZPyLGQBGRYCY2gxFDASBgoJkiaJk/IsZAEZFgRjZXJuMSUwIwYDVQQDExxDRVJOIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMTEwODA4Mzg1NVoXDTIzMDcyOTA5MTkzOFowVjESMBAGCgmSJomT8ixkARkWAmNoMRQwEgYKCZImiZPyLGQBGRYEY2VybjESMBAGA1UECxMJY29tcHV0ZXJzMRYwFAYDVQQDEw1sb2dpbi5jZXJuLmNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp6t1C0SGlLddL2M+ltffGioTnDT3eztOxlA9bAGuvB8/Rjym8en6+ET9boM02CyoR5Vpn8iElXVWccAExPIQEq70D6LPe86vb+tYhuKPeLfuICN9Z0SMQ4f+57vk61Co1/uw/8kPvXlyd+Ai8Dsn/G0hpH67bBI9VOQKfpJqclcSJuSlUB5PJffvMUpr29B0eRx8LKFnIHbDILSu6nVbFLcadtWIjbYvoKorXg3J6urtkz+zEDeYMTvA6ZGOFf/Xy5eGtroSq9csSC976tx+umKEPhXBA9AcpiCV9Cj5axN03Aaa+iTE36jpnjcd9d02dy5Q9jE2nUN6KXnB6qF6eQIDAQABo4ID5TCCA+EwPQYJKwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIg73QCYLtjQ2G7Ysrgd71N4WA0GIehd2yb4Wu9TkCAWQCARkwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDwEB/wQEAwIFoDBoBgNVHSAEYTBfMF0GCisGAQQBYAoEAQEwTzBNBggrBgEFBQcCARZBaHR0cDovL2NhLWRvY3MuY2Vybi5jaC9jYS1kb2NzL2NwLWNwcy9jZXJuLXRydXN0ZWQtY2EyLWNwLWNwcy5wZGYwJwYJKwYBBAGCNxUKBBowGDAKBggrBgEFBQcDAjAKBggrBgEFBQcDATAdBgNVHQ4EFgQUqtJcwUXasyM6sRaO5nCMFoFDenMwGAYDVR0RBBEwD4INbG9naW4uY2Vybi5jaDAfBgNVHSMEGDAWgBQdkBnqyM7MPI0UsUzZ7BTiYUADYTCCASoGA1UdHwSCASEwggEdMIIBGaCCARWgggERhkdodHRwOi8vY2FmaWxlcy5jZXJuLmNoL2NhZmlsZXMvY3JsL0NFUk4lMjBDZXJ0aWZpY2F0aW9uJTIwQXV0aG9yaXR5LmNybIaBxWxkYXA6Ly8vQ049Q0VSTiUyMENlcnRpZmljYXRpb24lMjBBdXRob3JpdHksQ049Q0VSTlBLSTA3LENOPUNEUCxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNlcyxDTj1TZXJ2aWNlcyxDTj1Db25maWd1cmF0aW9uLERDPWNlcm4sREM9Y2g/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNzPWNSTERpc3RyaWJ1dGlvblBvaW50MIIBVAYIKwYBBQUHAQEEggFGMIIBQjBcBggrBgEFBQcwAoZQaHR0cDovL2NhZmlsZXMuY2Vybi5jaC9jYWZpbGVzL2NlcnRpZmljYXRlcy9DRVJOJTIwQ2VydGlmaWNhdGlvbiUyMEF1dGhvcml0eS5jcnQwgbsGCCsGAQUFBzAChoGubGRhcDovLy9DTj1DRVJOJTIwQ2VydGlmaWNhdGlvbiUyMEF1dGhvcml0eSxDTj1BSUEsQ049UHVibGljJTIwS2V5JTIwU2VydmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1jZXJuLERDPWNoP2NBQ2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0aG9yaXR5MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jZXJuLmNoL29jc3AwDQYJKoZIhvcNAQELBQADggIBAGKZ3bknTCfNuh4TMaL3PuvBFjU8LQ5NKY9GLZvY2ibYMRk5Is6eWRgyUsy1UJRQdaQQPnnysqrGq8VRw/NIFotBBsA978/+jj7v4e5Kr4o8HvwAQNLBxNmF6XkDytpLL701FcNEGRqIsoIhNzihi2VBADLC9HxljEyPT52IR767TMk/+xTOqClceq3sq6WRD4m+xaWRUJyOhn+Pqr+wbhXIw4wzHC6X0hcLj8P9Povtm6VmKkN9JPuymMo/0+zSrUt2+TYfmbbEKYJSP0+sceQ76IKxxmSdKAr1qDNE8v+c3DvPM2PKmfivwaV2l44FdP8ulzqTgphkYcN1daa9Oc+qJeyu/eL7xWzk6Zq5R+jVrMlM0p1y2XczI7Hoc96TMOcbVnwgMcVqRM9p57VItn6XubYPR0C33i1yUZjkWbIfqEjq6Vev6lVgngOyzu+hqC/8SDyORA3dlF9aZOD13kPZdF/JRphHREQtaRydAiYRlE/WHTvOcY52jujDftUR6oY0eWaWkwSHbX+kDFx8IlR8UtQCUgkGHBGwnOYLIGu7SRDGSfOBOiVhxKoHWVk/pL6eKY2SkmyOmmgO4JnQGg95qeAOMG/EQZt/2x8GAavUqGvYy9dPFwFf08678hQqkjNSuex7UD0ku8OP1QKvpP44l6vZhFc6A5XqjdU9lus1 _c9e77bc4-a81b-4da7-88c2-72a6ba376d3f _c9e77bc4-a81b-4da7-88c2-72a6ba376d3f urn:oasis:names:tc:SAML:1.0:assertion http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/examples/xml/ADFS_fault.xml0000664000175000017500000000153300000000000030671 0ustar00zuulzuul00000000000000 http://www.w3.org/2005/08/addressing/soap/fault urn:uuid:89c47849-2622-4cdc-bb06-1d46c89ed12d s:Sender a:FailedAuthentication At least one security token in the message could not be validated. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3087943 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/fixtures/0000775000175000017500000000000000000000000025470 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/fixtures/__init__.py0000664000175000017500000000673500000000000027614 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import string DIR = os.path.dirname(os.path.abspath(__file__)) def template(f, **kwargs): with open(os.path.join(DIR, 'templates', f)) as f: return string.Template(f.read()).substitute(**kwargs) def soap_response(**kwargs): kwargs.setdefault('provider', 'https://idp.testshib.org/idp/shibboleth') kwargs.setdefault('consumer', 'https://openstack4.local/Shibboleth.sso/SAML2/ECP') kwargs.setdefault('issuer', 'https://openstack4.local/shibboleth') return template('soap_response.xml', **kwargs).encode('utf-8') def saml_assertion(**kwargs): kwargs.setdefault('issuer', 'https://idp.testshib.org/idp/shibboleth') kwargs.setdefault('destination', 'https://openstack4.local/Shibboleth.sso/SAML2/ECP') return template('saml_assertion.xml', **kwargs).encode('utf-8') def authn_request(**kwargs): kwargs.setdefault('issuer', 'https://openstack4.local/Shibboleth.sso/SAML2/ECP') return template('authn_request.xml', **kwargs).encode('utf-8') SP_SOAP_RESPONSE = soap_response() SAML2_ASSERTION = saml_assertion() AUTHN_REQUEST = authn_request() UNSCOPED_TOKEN_HEADER = 'UNSCOPED_TOKEN' UNSCOPED_TOKEN = { "token": { "issued_at": "2014-06-09T09:48:59.643406Z", "extras": {}, "methods": ["saml2"], "expires_at": "2014-06-09T10:48:59.643375Z", "user": { "OS-FEDERATION": { "identity_provider": { "id": "testshib" }, "protocol": { "id": "saml2" }, "groups": [ {"id": "1764fa5cf69a49a4918131de5ce4af9a"} ] }, "id": "testhib%20user", "name": "testhib user" } } } PROJECTS = { "projects": [ { "domain_id": "37ef61", "enabled": 'true', "id": "12d706", "links": { "self": "http://identity:35357/v3/projects/12d706" }, "name": "a project name" }, { "domain_id": "37ef61", "enabled": 'true', "id": "9ca0eb", "links": { "self": "http://identity:35357/v3/projects/9ca0eb" }, "name": "another project" } ], "links": { "self": "http://identity:35357/v3/OS-FEDERATION/projects", "previous": 'null', "next": 'null' } } DOMAINS = { "domains": [ { "description": "desc of domain", "enabled": 'true', "id": "37ef61", "links": { "self": "http://identity:35357/v3/domains/37ef61" }, "name": "my domain" } ], "links": { "self": "http://identity:35357/v3/OS-FEDERATION/domains", "previous": 'null', "next": 'null' } } ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3127944 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/fixtures/templates/0000775000175000017500000000000000000000000027466 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/fixtures/templates/authn_request.xml0000664000175000017500000000171300000000000033101 0ustar00zuulzuul00000000000000 $issuer ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/fixtures/templates/saml_assertion.xml0000664000175000017500000000716200000000000033241 0ustar00zuulzuul00000000000000 x= $issuer VALUE== VALUE= ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/fixtures/templates/soap_response.xml0000664000175000017500000000371100000000000033072 0ustar00zuulzuul00000000000000 $issuer ss:mem:6f1f20fafbb38433467e9d477df67615 $issuer ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/test_auth_adfs.py0000664000175000017500000002501200000000000027166 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from lxml import etree from six.moves import urllib from keystoneauth1 import exceptions from keystoneauth1.extras import _saml2 as saml2 from keystoneauth1.tests.unit import client_fixtures from keystoneauth1.tests.unit.extras.saml2 import fixtures as saml2_fixtures from keystoneauth1.tests.unit.extras.saml2 import utils from keystoneauth1.tests.unit import matchers class AuthenticateviaADFSTests(utils.TestCase): GROUP = 'auth' NAMESPACES = { 's': 'http://www.w3.org/2003/05/soap-envelope', 'trust': 'http://docs.oasis-open.org/ws-sx/ws-trust/200512', 'wsa': 'http://www.w3.org/2005/08/addressing', 'wsp': 'http://schemas.xmlsoap.org/ws/2004/09/policy', 'a': 'http://www.w3.org/2005/08/addressing', 'o': ('http://docs.oasis-open.org/wss/2004/01/oasis' '-200401-wss-wssecurity-secext-1.0.xsd') } USER_XPATH = ('/s:Envelope/s:Header' '/o:Security' '/o:UsernameToken' '/o:Username') PASSWORD_XPATH = ('/s:Envelope/s:Header' '/o:Security' '/o:UsernameToken' '/o:Password') ADDRESS_XPATH = ('/s:Envelope/s:Body' '/trust:RequestSecurityToken' '/wsp:AppliesTo/wsa:EndpointReference' '/wsa:Address') TO_XPATH = ('/s:Envelope/s:Header' '/a:To') TEST_TOKEN = uuid.uuid4().hex PROTOCOL = 'saml2' @property def _uuid4(self): return '4b911420-4982-4009-8afc-5c596cd487f5' def setUp(self): super(AuthenticateviaADFSTests, self).setUp() self.IDENTITY_PROVIDER = 'adfs' self.IDENTITY_PROVIDER_URL = ('http://adfs.local/adfs/service/trust/13' '/usernamemixed') self.FEDERATION_AUTH_URL = '%s/%s' % ( self.TEST_URL, 'OS-FEDERATION/identity_providers/adfs/protocols/saml2/auth') self.SP_ENDPOINT = 'https://openstack4.local/Shibboleth.sso/ADFS' self.SP_ENTITYID = 'https://openstack4.local' self.adfsplugin = saml2.V3ADFSPassword( self.TEST_URL, self.IDENTITY_PROVIDER, self.IDENTITY_PROVIDER_URL, self.SP_ENDPOINT, self.TEST_USER, self.TEST_TOKEN, self.PROTOCOL) self.ADFS_SECURITY_TOKEN_RESPONSE = utils._load_xml( 'ADFS_RequestSecurityTokenResponse.xml') self.ADFS_FAULT = utils._load_xml('ADFS_fault.xml') def test_get_adfs_security_token(self): """Test ADFSPassword._get_adfs_security_token().""" self.requests_mock.post( self.IDENTITY_PROVIDER_URL, content=utils.make_oneline(self.ADFS_SECURITY_TOKEN_RESPONSE), status_code=200) self.adfsplugin._prepare_adfs_request() self.adfsplugin._get_adfs_security_token(self.session) adfs_response = etree.tostring(self.adfsplugin.adfs_token) fixture_response = self.ADFS_SECURITY_TOKEN_RESPONSE self.assertThat(fixture_response, matchers.XMLEquals(adfs_response)) def test_adfs_request_user(self): self.adfsplugin._prepare_adfs_request() user = self.adfsplugin.prepared_request.xpath( self.USER_XPATH, namespaces=self.NAMESPACES)[0] self.assertEqual(self.TEST_USER, user.text) def test_adfs_request_password(self): self.adfsplugin._prepare_adfs_request() password = self.adfsplugin.prepared_request.xpath( self.PASSWORD_XPATH, namespaces=self.NAMESPACES)[0] self.assertEqual(self.TEST_TOKEN, password.text) def test_adfs_request_to(self): self.adfsplugin._prepare_adfs_request() to = self.adfsplugin.prepared_request.xpath( self.TO_XPATH, namespaces=self.NAMESPACES)[0] self.assertEqual(self.IDENTITY_PROVIDER_URL, to.text) def test_prepare_adfs_request_address(self): self.adfsplugin._prepare_adfs_request() address = self.adfsplugin.prepared_request.xpath( self.ADDRESS_XPATH, namespaces=self.NAMESPACES)[0] self.assertEqual(self.SP_ENDPOINT, address.text) def test_prepare_adfs_request_custom_endpointreference(self): self.adfsplugin = saml2.V3ADFSPassword( self.TEST_URL, self.IDENTITY_PROVIDER, self.IDENTITY_PROVIDER_URL, self.SP_ENDPOINT, self.TEST_USER, self.TEST_TOKEN, self.PROTOCOL, self.SP_ENTITYID) self.adfsplugin._prepare_adfs_request() address = self.adfsplugin.prepared_request.xpath( self.ADDRESS_XPATH, namespaces=self.NAMESPACES)[0] self.assertEqual(self.SP_ENTITYID, address.text) def test_prepare_sp_request(self): assertion = etree.XML(self.ADFS_SECURITY_TOKEN_RESPONSE) assertion = assertion.xpath( saml2.V3ADFSPassword.ADFS_ASSERTION_XPATH, namespaces=saml2.V3ADFSPassword.ADFS_TOKEN_NAMESPACES) assertion = assertion[0] assertion = etree.tostring(assertion) assertion = assertion.replace( b'http://docs.oasis-open.org/ws-sx/ws-trust/200512', b'http://schemas.xmlsoap.org/ws/2005/02/trust') assertion = urllib.parse.quote(assertion) assertion = 'wa=wsignin1.0&wresult=' + assertion self.adfsplugin.adfs_token = etree.XML( self.ADFS_SECURITY_TOKEN_RESPONSE) self.adfsplugin._prepare_sp_request() self.assertEqual(assertion, self.adfsplugin.encoded_assertion) def test_get_adfs_security_token_authn_fail(self): """Test proper parsing XML fault after bad authentication. An exceptions.AuthorizationFailure should be raised including error message from the XML message indicating where was the problem. """ content = utils.make_oneline(self.ADFS_FAULT) self.requests_mock.register_uri('POST', self.IDENTITY_PROVIDER_URL, content=content, status_code=500) self.adfsplugin._prepare_adfs_request() self.assertRaises(exceptions.AuthorizationFailure, self.adfsplugin._get_adfs_security_token, self.session) # TODO(marek-denis): Python3 tests complain about missing 'message' # attributes # self.assertEqual('a:FailedAuthentication', e.message) def test_get_adfs_security_token_bad_response(self): """Test proper handling HTTP 500 and mangled (non XML) response. This should never happen yet, keystoneauth1 should be prepared and correctly raise exceptions.InternalServerError once it cannot parse XML fault message """ self.requests_mock.register_uri('POST', self.IDENTITY_PROVIDER_URL, content=b'NOT XML', status_code=500) self.adfsplugin._prepare_adfs_request() self.assertRaises(exceptions.InternalServerError, self.adfsplugin._get_adfs_security_token, self.session) # TODO(marek-denis): Need to figure out how to properly send cookies # from the request_mock methods. def _send_assertion_to_service_provider(self): """Test whether SP issues a cookie.""" cookie = uuid.uuid4().hex self.requests_mock.post(self.SP_ENDPOINT, headers={"set-cookie": cookie}, status_code=302) self.adfsplugin.adfs_token = self._build_adfs_request() self.adfsplugin._prepare_sp_request() self.adfsplugin._send_assertion_to_service_provider(self.session) self.assertEqual(1, len(self.session.session.cookies)) def test_send_assertion_to_service_provider_bad_status(self): self.requests_mock.register_uri('POST', self.SP_ENDPOINT, status_code=500) self.adfsplugin.adfs_token = etree.XML( self.ADFS_SECURITY_TOKEN_RESPONSE) self.adfsplugin._prepare_sp_request() self.assertRaises( exceptions.InternalServerError, self.adfsplugin._send_assertion_to_service_provider, self.session) def test_access_sp_no_cookies_fail(self): # clean cookie jar self.session.session.cookies = [] self.assertRaises(exceptions.AuthorizationFailure, self.adfsplugin._access_service_provider, self.session) def test_check_valid_token_when_authenticated(self): self.requests_mock.register_uri( 'GET', self.FEDERATION_AUTH_URL, json=saml2_fixtures.UNSCOPED_TOKEN, headers=client_fixtures.AUTH_RESPONSE_HEADERS) self.session.session.cookies = [object()] self.adfsplugin._access_service_provider(self.session) response = self.adfsplugin.authenticated_response self.assertEqual(client_fixtures.AUTH_RESPONSE_HEADERS, response.headers) self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], response.json()['token']) def test_end_to_end_workflow(self): self.requests_mock.register_uri( 'POST', self.IDENTITY_PROVIDER_URL, content=self.ADFS_SECURITY_TOKEN_RESPONSE, status_code=200) self.requests_mock.register_uri( 'POST', self.SP_ENDPOINT, headers={"set-cookie": 'x'}, status_code=302) self.requests_mock.register_uri( 'GET', self.FEDERATION_AUTH_URL, json=saml2_fixtures.UNSCOPED_TOKEN, headers=client_fixtures.AUTH_RESPONSE_HEADERS) # NOTE(marek-denis): We need to mimic this until self.requests_mock can # issue cookies properly. self.session.session.cookies = [object()] token = self.adfsplugin.get_auth_ref(self.session) self.assertEqual(client_fixtures.AUTH_SUBJECT_TOKEN, token.auth_token) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/test_auth_saml2.py0000664000175000017500000003002100000000000027263 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import base64 import uuid import requests from keystoneauth1 import exceptions from keystoneauth1.extras import _saml2 as saml2 from keystoneauth1 import fixture as ksa_fixtures from keystoneauth1 import session from keystoneauth1.tests.unit.extras.saml2 import fixtures as saml2_fixtures from keystoneauth1.tests.unit.extras.saml2 import utils from keystoneauth1.tests.unit import matchers PAOS_HEADER = 'application/vnd.paos+xml' CONTENT_TYPE_PAOS_HEADER = {'Content-Type': PAOS_HEADER} InvalidResponse = saml2.v3.saml2.InvalidResponse class SamlAuth2PluginTests(utils.TestCase): """These test ONLY the standalone requests auth plugin. Tests for the auth plugin are later so that hopefully these can be extracted into it's own module. """ HEADER_MEDIA_TYPE_SEPARATOR = ',' TEST_USER = 'user' TEST_PASS = 'pass' TEST_SP_URL = 'http://sp.test' TEST_IDP_URL = 'http://idp.test' TEST_CONSUMER_URL = "https://openstack4.local/Shibboleth.sso/SAML2/ECP" def get_plugin(self, **kwargs): kwargs.setdefault('identity_provider_url', self.TEST_IDP_URL) kwargs.setdefault('requests_auth', (self.TEST_USER, self.TEST_PASS)) return saml2.v3.saml2._SamlAuth(**kwargs) @property def calls(self): return [r.url.strip('/') for r in self.requests_mock.request_history] def basic_header(self, username=TEST_USER, password=TEST_PASS): user_pass = ('%s:%s' % (username, password)).encode('utf-8') return 'Basic %s' % base64.b64encode(user_pass).decode('utf-8') def test_request_accept_headers(self): # Include some random Accept header random_header = uuid.uuid4().hex headers = {'Accept': random_header} req = requests.Request('GET', 'http://another.test', headers=headers) plugin = self.get_plugin() plugin_headers = plugin(req).headers self.assertIn('Accept', plugin_headers) # Since we have included a random Accept header, the plugin should have # added the PAOS_HEADER to it using the correct media type separator accept_header = plugin_headers['Accept'] self.assertIn(self.HEADER_MEDIA_TYPE_SEPARATOR, accept_header) self.assertIn(random_header, accept_header.split(self.HEADER_MEDIA_TYPE_SEPARATOR)) self.assertIn(PAOS_HEADER, accept_header.split(self.HEADER_MEDIA_TYPE_SEPARATOR)) def test_passed_when_not_200(self): text = uuid.uuid4().hex test_url = 'http://another.test' self.requests_mock.get(test_url, status_code=201, headers=CONTENT_TYPE_PAOS_HEADER, text=text) resp = requests.get(test_url, auth=self.get_plugin()) self.assertEqual(201, resp.status_code) self.assertEqual(text, resp.text) def test_200_without_paos_header(self): text = uuid.uuid4().hex test_url = 'http://another.test' self.requests_mock.get(test_url, status_code=200, text=text) resp = requests.get(test_url, auth=self.get_plugin()) self.assertEqual(200, resp.status_code) self.assertEqual(text, resp.text) def test_standard_workflow_302_redirect(self): text = uuid.uuid4().hex self.requests_mock.get(self.TEST_SP_URL, response_list=[ dict(headers=CONTENT_TYPE_PAOS_HEADER, content=utils.make_oneline(saml2_fixtures.SP_SOAP_RESPONSE)), dict(text=text) ]) authm = self.requests_mock.post(self.TEST_IDP_URL, content=saml2_fixtures.SAML2_ASSERTION) self.requests_mock.post( self.TEST_CONSUMER_URL, status_code=302, headers={'Location': self.TEST_SP_URL}) resp = requests.get(self.TEST_SP_URL, auth=self.get_plugin()) self.assertEqual(200, resp.status_code) self.assertEqual(text, resp.text) self.assertEqual(self.calls, [self.TEST_SP_URL, self.TEST_IDP_URL, self.TEST_CONSUMER_URL, self.TEST_SP_URL]) self.assertEqual(self.basic_header(), authm.last_request.headers['Authorization']) authn_request = self.requests_mock.request_history[1].text self.assertThat(saml2_fixtures.AUTHN_REQUEST, matchers.XMLEquals(authn_request)) def test_standard_workflow_303_redirect(self): text = uuid.uuid4().hex self.requests_mock.get(self.TEST_SP_URL, response_list=[ dict(headers=CONTENT_TYPE_PAOS_HEADER, content=utils.make_oneline(saml2_fixtures.SP_SOAP_RESPONSE)), dict(text=text) ]) authm = self.requests_mock.post(self.TEST_IDP_URL, content=saml2_fixtures.SAML2_ASSERTION) self.requests_mock.post( self.TEST_CONSUMER_URL, status_code=303, headers={'Location': self.TEST_SP_URL}) resp = requests.get(self.TEST_SP_URL, auth=self.get_plugin()) self.assertEqual(200, resp.status_code) self.assertEqual(text, resp.text) url_flow = [self.TEST_SP_URL, self.TEST_IDP_URL, self.TEST_CONSUMER_URL, self.TEST_SP_URL] self.assertEqual(url_flow, [r.url.rstrip('/') for r in resp.history]) self.assertEqual(url_flow, self.calls) self.assertEqual(self.basic_header(), authm.last_request.headers['Authorization']) authn_request = self.requests_mock.request_history[1].text self.assertThat(saml2_fixtures.AUTHN_REQUEST, matchers.XMLEquals(authn_request)) def test_initial_sp_call_invalid_response(self): """Send initial SP HTTP request and receive wrong server response.""" self.requests_mock.get(self.TEST_SP_URL, headers=CONTENT_TYPE_PAOS_HEADER, text='NON XML RESPONSE') self.assertRaises(InvalidResponse, requests.get, self.TEST_SP_URL, auth=self.get_plugin()) self.assertEqual(self.calls, [self.TEST_SP_URL]) def test_consumer_mismatch_error_workflow(self): consumer1 = 'http://consumer1/Shibboleth.sso/SAML2/ECP' consumer2 = 'http://consumer2/Shibboleth.sso/SAML2/ECP' soap_response = saml2_fixtures.soap_response(consumer=consumer1) saml_assertion = saml2_fixtures.saml_assertion(destination=consumer2) self.requests_mock.get(self.TEST_SP_URL, headers=CONTENT_TYPE_PAOS_HEADER, content=soap_response) self.requests_mock.post(self.TEST_IDP_URL, content=saml_assertion) # receive the SAML error, body unchecked saml_error = self.requests_mock.post(consumer1) self.assertRaises(saml2.v3.saml2.ConsumerMismatch, requests.get, self.TEST_SP_URL, auth=self.get_plugin()) self.assertTrue(saml_error.called) class AuthenticateviaSAML2Tests(utils.TestCase): TEST_USER = 'user' TEST_PASS = 'pass' TEST_IDP = 'tester' TEST_PROTOCOL = 'saml2' TEST_AUTH_URL = 'http://keystone.test:5000/v3/' TEST_IDP_URL = 'https://idp.test' TEST_CONSUMER_URL = "https://openstack4.local/Shibboleth.sso/SAML2/ECP" def get_plugin(self, **kwargs): kwargs.setdefault('auth_url', self.TEST_AUTH_URL) kwargs.setdefault('username', self.TEST_USER) kwargs.setdefault('password', self.TEST_PASS) kwargs.setdefault('identity_provider', self.TEST_IDP) kwargs.setdefault('identity_provider_url', self.TEST_IDP_URL) kwargs.setdefault('protocol', self.TEST_PROTOCOL) return saml2.V3Saml2Password(**kwargs) def sp_url(self, **kwargs): kwargs.setdefault('base', self.TEST_AUTH_URL.rstrip('/')) kwargs.setdefault('identity_provider', self.TEST_IDP) kwargs.setdefault('protocol', self.TEST_PROTOCOL) templ = ('%(base)s/OS-FEDERATION/identity_providers/' '%(identity_provider)s/protocols/%(protocol)s/auth') return templ % kwargs @property def calls(self): return [r.url.strip('/') for r in self.requests_mock.request_history] def basic_header(self, username=TEST_USER, password=TEST_PASS): user_pass = ('%s:%s' % (username, password)).encode('utf-8') return 'Basic %s' % base64.b64encode(user_pass).decode('utf-8') def setUp(self): super(AuthenticateviaSAML2Tests, self).setUp() self.session = session.Session() self.default_sp_url = self.sp_url() def test_workflow(self): token_id = uuid.uuid4().hex token = ksa_fixtures.V3Token() self.requests_mock.get(self.default_sp_url, response_list=[ dict(headers=CONTENT_TYPE_PAOS_HEADER, content=utils.make_oneline(saml2_fixtures.SP_SOAP_RESPONSE)), dict(headers={'X-Subject-Token': token_id}, json=token) ]) authm = self.requests_mock.post(self.TEST_IDP_URL, content=saml2_fixtures.SAML2_ASSERTION) self.requests_mock.post( self.TEST_CONSUMER_URL, status_code=302, headers={'Location': self.sp_url()}) auth_ref = self.get_plugin().get_auth_ref(self.session) self.assertEqual(token_id, auth_ref.auth_token) self.assertEqual(self.calls, [self.default_sp_url, self.TEST_IDP_URL, self.TEST_CONSUMER_URL, self.default_sp_url]) self.assertEqual(self.basic_header(), authm.last_request.headers['Authorization']) authn_request = self.requests_mock.request_history[1].text self.assertThat(saml2_fixtures.AUTHN_REQUEST, matchers.XMLEquals(authn_request)) def test_consumer_mismatch_error_workflow(self): consumer1 = 'http://keystone.test/Shibboleth.sso/SAML2/ECP' consumer2 = 'http://consumer2/Shibboleth.sso/SAML2/ECP' soap_response = saml2_fixtures.soap_response(consumer=consumer1) saml_assertion = saml2_fixtures.saml_assertion(destination=consumer2) self.requests_mock.get(self.default_sp_url, headers=CONTENT_TYPE_PAOS_HEADER, content=soap_response) self.requests_mock.post(self.TEST_IDP_URL, content=saml_assertion) # receive the SAML error, body unchecked saml_error = self.requests_mock.post(consumer1) self.assertRaises(exceptions.AuthorizationFailure, self.get_plugin().get_auth_ref, self.session) self.assertTrue(saml_error.called) def test_initial_sp_call_invalid_response(self): """Send initial SP HTTP request and receive wrong server response.""" self.requests_mock.get(self.default_sp_url, headers=CONTENT_TYPE_PAOS_HEADER, text='NON XML RESPONSE') self.assertRaises(exceptions.AuthorizationFailure, self.get_plugin().get_auth_ref, self.session) self.assertEqual(self.calls, [self.default_sp_url]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/extras/saml2/utils.py0000664000175000017500000000214000000000000025326 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os from lxml import etree from keystoneauth1 import session from keystoneauth1.tests.unit import utils ROOTDIR = os.path.dirname(os.path.abspath(__file__)) XMLDIR = os.path.join(ROOTDIR, 'examples', 'xml/') def make_oneline(s): return etree.tostring(etree.XML(s)).replace(b'\n', b'') def _load_xml(filename): with open(XMLDIR + filename, 'rb') as f: return f.read() class TestCase(utils.TestCase): TEST_URL = 'https://keystone:5000/v3' def setUp(self): super(TestCase, self).setUp() self.session = session.Session() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3127944 keystoneauth1-4.4.0/keystoneauth1/tests/unit/identity/0000775000175000017500000000000000000000000023124 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/identity/__init__.py0000664000175000017500000000000000000000000025223 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/identity/test_access.py0000664000175000017500000000532100000000000025777 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from keystoneauth1 import access from keystoneauth1 import fixture from keystoneauth1.identity import access as access_plugin from keystoneauth1 import plugin from keystoneauth1 import session from keystoneauth1.tests.unit import utils class AccessInfoPluginTests(utils.TestCase): def setUp(self): super(AccessInfoPluginTests, self).setUp() self.session = session.Session() self.auth_token = uuid.uuid4().hex def _plugin(self, **kwargs): token = fixture.V3Token() s = token.add_service('identity') s.add_standard_endpoints(public=self.TEST_ROOT_URL) auth_ref = access.create(body=token, auth_token=self.auth_token) return access_plugin.AccessInfoPlugin(auth_ref, **kwargs) def test_auth_ref(self): plugin_obj = self._plugin() self.assertEqual(self.TEST_ROOT_URL, plugin_obj.get_endpoint(self.session, service_type='identity', interface='public')) self.assertEqual(self.auth_token, plugin_obj.get_token(session)) def test_auth_url(self): auth_url = 'http://keystone.test.url' obj = self._plugin(auth_url=auth_url) self.assertEqual(auth_url, obj.get_endpoint(self.session, interface=plugin.AUTH_INTERFACE)) def test_invalidate(self): plugin = self._plugin() auth_ref = plugin.auth_ref self.assertIsInstance(auth_ref, access.AccessInfo) self.assertFalse(plugin.invalidate()) self.assertIs(auth_ref, plugin.auth_ref) def test_project_auth_properties(self): plugin = self._plugin() auth_ref = plugin.auth_ref self.assertIsNone(auth_ref.project_domain_id) self.assertIsNone(auth_ref.project_domain_name) self.assertIsNone(auth_ref.project_id) self.assertIsNone(auth_ref.project_name) def test_domain_auth_properties(self): plugin = self._plugin() auth_ref = plugin.auth_ref self.assertIsNone(auth_ref.domain_id) self.assertIsNone(auth_ref.domain_name) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/identity/test_identity_common.py0000664000175000017500000027062300000000000027750 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc import collections import uuid import six from six.moves import urllib from keystoneauth1 import _utils from keystoneauth1 import access from keystoneauth1 import adapter from keystoneauth1 import discover from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1 import identity from keystoneauth1 import plugin from keystoneauth1 import session from keystoneauth1.tests.unit import utils _Endpoints = collections.namedtuple( 'ServiceVersion', 'public, internal, admin') _ServiceVersion = collections.namedtuple( 'ServiceVersion', 'discovery, service') class FakeServiceEndpoints(object): def __init__(self, base_url, versions=None, project_id=None, **kwargs): self.base_url = base_url self._interfaces = {} for interface in ('public', 'internal', 'admin'): if interface in kwargs and not kwargs[interface]: self._interfaces[interface] = False else: self._interfaces[interface] = True self.versions = {} self.unversioned = self._make_urls() if not versions: self.catalog = self.unversioned else: self.catalog = self._make_urls(versions[0], project_id) for version in versions: self.versions[version] = _ServiceVersion( self._make_urls(version), self._make_urls(version, project_id), ) def _make_urls(self, *parts): return _Endpoints( self._make_url('public', *parts), self._make_url('internal', *parts), self._make_url('admin', *parts), ) def _make_url(self, interface, *parts): if not self._interfaces[interface]: return None url = urllib.parse.urljoin(self.base_url + '/', interface) for part in parts: if part: url = urllib.parse.urljoin(url + '/', part) return url @six.add_metaclass(abc.ABCMeta) class CommonIdentityTests(object): PROJECT_ID = uuid.uuid4().hex TEST_ROOT_URL = 'http://127.0.0.1:5000/' TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' TEST_COMPUTE_BASE = 'https://compute.example.com' TEST_COMPUTE_PUBLIC = TEST_COMPUTE_BASE + '/nova/public' TEST_COMPUTE_INTERNAL = TEST_COMPUTE_BASE + '/nova/internal' TEST_COMPUTE_ADMIN = TEST_COMPUTE_BASE + '/nova/admin' TEST_VOLUME = FakeServiceEndpoints( base_url='https://block-storage.example.com', versions=['v3', 'v2'], project_id=PROJECT_ID) TEST_BAREMETAL_BASE = 'https://baremetal.example.com' TEST_BAREMETAL_INTERNAL = TEST_BAREMETAL_BASE + '/internal' TEST_PASS = uuid.uuid4().hex def setUp(self): super(CommonIdentityTests, self).setUp() self.TEST_URL = '%s%s' % (self.TEST_ROOT_URL, self.version) self.TEST_ADMIN_URL = '%s%s' % (self.TEST_ROOT_ADMIN_URL, self.version) self.TEST_DISCOVERY = fixture.DiscoveryList(href=self.TEST_ROOT_URL) self.stub_auth_data() @abc.abstractmethod def create_auth_plugin(self, **kwargs): """Create an auth plugin that makes sense for the auth data. It doesn't really matter what auth mechanism is used but it should be appropriate to the API version. """ @abc.abstractmethod def get_auth_data(self, **kwargs): """Return fake authentication data. This should register a valid token response and ensure that the compute endpoints are set to TEST_COMPUTE_PUBLIC, _INTERNAL and _ADMIN. """ def stub_auth_data(self, **kwargs): token = self.get_auth_data(**kwargs) self.user_id = token.user_id try: self.project_id = token.project_id except AttributeError: self.project_id = token.tenant_id self.stub_auth(json=token) @abc.abstractproperty def version(self): """The API version being tested.""" def test_discovering(self): disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_nova_microversion( href=self.TEST_COMPUTE_ADMIN, id='v2.1', status='CURRENT', min_version='2.1', version='2.38') self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, json=disc) body = 'SUCCESS' # which gives our sample values self.stub_url('GET', ['path'], text=body, base_url=self.TEST_COMPUTE_ADMIN) a = self.create_auth_plugin() s = session.Session(auth=a) resp = s.get('/path', endpoint_filter={'service_type': 'compute', 'interface': 'admin', 'version': '2.1'}) self.assertEqual(200, resp.status_code) self.assertEqual(body, resp.text) new_body = 'SC SUCCESS' # if we don't specify a version, we use the URL from the SC self.stub_url('GET', ['path'], base_url=self.TEST_COMPUTE_ADMIN, text=new_body) resp = s.get('/path', endpoint_filter={'service_type': 'compute', 'interface': 'admin'}) self.assertEqual(200, resp.status_code) self.assertEqual(new_body, resp.text) def test_discovery_uses_provided_session_cache(self): disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_nova_microversion( href=self.TEST_COMPUTE_ADMIN, id='v2.1', status='CURRENT', min_version='2.1', version='2.38') # register responses such that if the discovery URL is hit more than # once then the response will be invalid and not point to COMPUTE_ADMIN resps = [{'json': disc}, {'status_code': 500}] self.requests_mock.get(self.TEST_COMPUTE_ADMIN, resps) body = 'SUCCESS' self.stub_url('GET', ['path'], text=body, base_url=self.TEST_COMPUTE_ADMIN) cache = {} # now either of the two plugins I use, it should not cause a second # request to the discovery url. s = session.Session(discovery_cache=cache) a = self.create_auth_plugin() b = self.create_auth_plugin() for auth in (a, b): resp = s.get('/path', auth=auth, endpoint_filter={'service_type': 'compute', 'interface': 'admin', 'version': '2.1'}) self.assertEqual(200, resp.status_code) self.assertEqual(body, resp.text) self.assertIn(self.TEST_COMPUTE_ADMIN, cache.keys()) def test_discovery_uses_session_cache(self): disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_nova_microversion( href=self.TEST_COMPUTE_ADMIN, id='v2.1', status='CURRENT', min_version='2.1', version='2.38') # register responses such that if the discovery URL is hit more than # once then the response will be invalid and not point to COMPUTE_ADMIN resps = [{'json': disc}, {'status_code': 500}] self.requests_mock.get(self.TEST_COMPUTE_ADMIN, resps) body = 'SUCCESS' self.stub_url('GET', ['path'], base_url=self.TEST_COMPUTE_ADMIN, text=body) filter = {'service_type': 'compute', 'interface': 'admin', 'version': '2.1'} # create a session and call the endpoint, causing its cache to be set sess = session.Session() sess.get('/path', auth=self.create_auth_plugin(), endpoint_filter=filter) self.assertIn(self.TEST_COMPUTE_ADMIN, sess._discovery_cache.keys()) # now either of the two plugins I use, it should not cause a second # request to the discovery url. a = self.create_auth_plugin() b = self.create_auth_plugin() for auth in (a, b): resp = sess.get('/path', auth=auth, endpoint_filter=filter) self.assertEqual(200, resp.status_code) self.assertEqual(body, resp.text) def test_discovery_uses_plugin_cache(self): disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_nova_microversion( href=self.TEST_COMPUTE_ADMIN, id='v2.1', status='CURRENT', min_version='2.1', version='2.38') # register responses such that if the discovery URL is hit more than # once then the response will be invalid and not point to COMPUTE_ADMIN resps = [{'json': disc}, {'status_code': 500}] self.requests_mock.get(self.TEST_COMPUTE_ADMIN, resps) body = 'SUCCESS' self.stub_url('GET', ['path'], base_url=self.TEST_COMPUTE_ADMIN, text=body) # now either of the two sessions I use, it should not cause a second # request to the discovery url. Calling discovery directly should also # not cause an additional request. sa = session.Session() sb = session.Session() auth = self.create_auth_plugin() for sess in (sa, sb): resp = sess.get('/path', auth=auth, endpoint_filter={'service_type': 'compute', 'interface': 'admin', 'version': '2.1'}) self.assertEqual(200, resp.status_code) self.assertEqual(body, resp.text) def test_discovery_uses_session_plugin_cache(self): disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_nova_microversion( href=self.TEST_COMPUTE_ADMIN, id='v2.1', status='CURRENT', min_version='2.1', version='2.38') # register responses such that if the discovery URL is hit more than # once then the response will be invalid and not point to COMPUTE_ADMIN resps = [{'json': disc}, {'status_code': 500}] self.requests_mock.get(self.TEST_COMPUTE_ADMIN, resps) body = 'SUCCESS' self.stub_url('GET', ['path'], base_url=self.TEST_COMPUTE_ADMIN, text=body) filter = {'service_type': 'compute', 'interface': 'admin', 'version': '2.1'} # create a plugin and call the endpoint, causing its cache to be set plugin = self.create_auth_plugin() session.Session().get('/path', auth=plugin, endpoint_filter=filter) self.assertIn(self.TEST_COMPUTE_ADMIN, plugin._discovery_cache.keys()) # with the plugin in the session, no more calls to the discovery URL sess = session.Session(auth=plugin) for auth in (plugin, self.create_auth_plugin()): resp = sess.get('/path', auth=auth, endpoint_filter=filter) self.assertEqual(200, resp.status_code) self.assertEqual(body, resp.text) def test_direct_discovery_provided_plugin_cache(self): # register responses such that if the discovery URL is hit more than # once then the response will be invalid and not point to COMPUTE_ADMIN resps = [{'json': self.TEST_DISCOVERY}, {'status_code': 500}] self.requests_mock.get(self.TEST_COMPUTE_ADMIN, resps) # now either of the two sessions I use, it should not cause a second # request to the discovery url. Calling discovery directly should also # not cause an additional request. sa = session.Session() sb = session.Session() discovery_cache = {} expected_url = self.TEST_COMPUTE_ADMIN + '/v2.0' for sess in (sa, sb): disc = discover.get_discovery( sess, self.TEST_COMPUTE_ADMIN, cache=discovery_cache) url = disc.url_for(('2', '0')) self.assertEqual(expected_url, url) self.assertIn(self.TEST_COMPUTE_ADMIN, discovery_cache.keys()) def test_discovery_trailing_slash(self): # The discovery cache should treat root urls the same whether they have # a slash or not. If the url is called a second time (meaning the cache # didn't work, we'll hit the 500 error. self.requests_mock.get( 'https://example.com', [ {'json': self.TEST_DISCOVERY}, {'status_code': 500} ]) sess = session.Session() discovery_cache = {} expected_url = 'https://example.com/v2.0' for test_endpoint in ('https://example.com', 'https://example.com/'): disc = discover.get_discovery( sess, test_endpoint, cache=discovery_cache) url = disc.url_for(('2', '0')) self.assertEqual(expected_url, url) self.assertIn('https://example.com', discovery_cache.keys()) self.assertNotIn('https://example.com/', discovery_cache.keys()) def test_discovering_with_no_data(self): # which returns discovery information pointing to TEST_URL but there is # no data there. self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, status_code=400) # so the url that will be used is the same TEST_COMPUTE_ADMIN body = 'SUCCESS' self.stub_url('GET', ['path'], base_url=self.TEST_COMPUTE_ADMIN, text=body, status_code=200) a = self.create_auth_plugin() s = session.Session(auth=a) resp = s.get('/path', endpoint_filter={'service_type': 'compute', 'interface': 'admin', 'version': self.version}) self.assertEqual(200, resp.status_code) self.assertEqual(body, resp.text) def test_direct_discovering_with_no_data(self): # returns discovery information pointing to TEST_URL but there is # no data there. self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, status_code=400) a = self.create_auth_plugin() s = session.Session(auth=a) # A direct call for discovery should fail self.assertRaises(exceptions.BadRequest, discover.get_discovery, s, self.TEST_COMPUTE_ADMIN) def test_discovering_with_relative_link(self): # need to construct list this way for relative disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_v2('v2.0') disc.add_v3('v3') self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, json=disc) a = self.create_auth_plugin() s = session.Session(auth=a) endpoint_v2 = s.get_endpoint(service_type='compute', interface='admin', version=(2, 0)) endpoint_v3 = s.get_endpoint(service_type='compute', interface='admin', version=(3, 0)) self.assertEqual(self.TEST_COMPUTE_ADMIN + '/v2.0', endpoint_v2) self.assertEqual(self.TEST_COMPUTE_ADMIN + '/v3', endpoint_v3) def test_direct_discovering(self): v2_compute = self.TEST_COMPUTE_ADMIN + '/v2.0' v3_compute = self.TEST_COMPUTE_ADMIN + '/v3' disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_v2(v2_compute) disc.add_v3(v3_compute) self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, json=disc) a = self.create_auth_plugin() s = session.Session(auth=a) catalog_url = s.get_endpoint( service_type='compute', interface='admin') disc = discover.get_discovery(s, catalog_url) url_v2 = disc.url_for(('2', '0')) url_v3 = disc.url_for(('3', '0')) self.assertEqual(v2_compute, url_v2) self.assertEqual(v3_compute, url_v3) # Verify that passing strings and not tuples works url_v2 = disc.url_for('2.0') url_v3 = disc.url_for('3.0') self.assertEqual(v2_compute, url_v2) self.assertEqual(v3_compute, url_v3) def test_discovering_version_no_discovery(self): a = self.create_auth_plugin() s = session.Session(auth=a) # Grab a version that can be returned without doing discovery # This tests that it doesn't make a discovery call because we don't # have a reqquest mock, and this will throw an exception if it tries version = s.get_api_major_version( service_type='volumev2', interface='admin') self.assertEqual((2, 0), version) def test_discovering_version_with_discovery(self): a = self.create_auth_plugin() s = session.Session(auth=a) v2_compute = self.TEST_COMPUTE_ADMIN + '/v2.0' v3_compute = self.TEST_COMPUTE_ADMIN + '/v3' disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_v2(v2_compute) disc.add_v3(v3_compute) self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, json=disc) # This needs to do version discovery to find the version version = s.get_api_major_version( service_type='compute', interface='admin') self.assertEqual((3, 0), version) self.assertEqual( self.requests_mock.request_history[-1].url, self.TEST_COMPUTE_ADMIN) def test_direct_discovering_with_relative_link(self): # need to construct list this way for relative disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_v2('v2.0') disc.add_v3('v3') self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, json=disc) a = self.create_auth_plugin() s = session.Session(auth=a) catalog_url = s.get_endpoint( service_type='compute', interface='admin') disc = discover.get_discovery(s, catalog_url) url_v2 = disc.url_for(('2', '0')) url_v3 = disc.url_for(('3', '0')) self.assertEqual(self.TEST_COMPUTE_ADMIN + '/v2.0', url_v2) self.assertEqual(self.TEST_COMPUTE_ADMIN + '/v3', url_v3) # Verify that passing strings and not tuples works url_v2 = disc.url_for('2.0') url_v3 = disc.url_for('3.0') self.assertEqual(self.TEST_COMPUTE_ADMIN + '/v2.0', url_v2) self.assertEqual(self.TEST_COMPUTE_ADMIN + '/v3', url_v3) def test_discovering_with_relative_anchored_link(self): # need to construct list this way for relative disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_v2('/v2.0') disc.add_v3('/v3') self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, json=disc) a = self.create_auth_plugin() s = session.Session(auth=a) endpoint_v2 = s.get_endpoint(service_type='compute', interface='admin', version=(2, 0)) endpoint_v3 = s.get_endpoint(service_type='compute', interface='admin', version=(3, 0)) # by the nature of urljoin a relative link with a /path gets joined # back to the root. self.assertEqual(self.TEST_COMPUTE_BASE + '/v2.0', endpoint_v2) self.assertEqual(self.TEST_COMPUTE_BASE + '/v3', endpoint_v3) def test_discovering_with_protocol_relative(self): # strip up to and including the : leaving //host/path path = self.TEST_COMPUTE_ADMIN[self.TEST_COMPUTE_ADMIN.find(':') + 1:] disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_v2(path + '/v2.0') disc.add_v3(path + '/v3') self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, json=disc) a = self.create_auth_plugin() s = session.Session(auth=a) endpoint_v2 = s.get_endpoint(service_type='compute', interface='admin', version=(2, 0)) endpoint_v3 = s.get_endpoint(service_type='compute', interface='admin', version=(3, 0)) # ensures that the http is carried over from the lookup url self.assertEqual(self.TEST_COMPUTE_ADMIN + '/v2.0', endpoint_v2) self.assertEqual(self.TEST_COMPUTE_ADMIN + '/v3', endpoint_v3) def test_discovering_when_version_missing(self): # need to construct list this way for relative disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_v2('v2.0') self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, json=disc) a = self.create_auth_plugin() s = session.Session(auth=a) endpoint_v2 = s.get_endpoint(service_type='compute', interface='admin', version=(2, 0)) endpoint_v3 = s.get_endpoint(service_type='compute', interface='admin', version=(3, 0)) self.assertEqual(self.TEST_COMPUTE_ADMIN + '/v2.0', endpoint_v2) self.assertIsNone(endpoint_v3) def test_endpoint_data_no_version(self): path = self.TEST_COMPUTE_ADMIN[self.TEST_COMPUTE_ADMIN.find(':') + 1:] disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_v2(path + '/v2.0') disc.add_v3(path + '/v3') self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, json=disc) a = self.create_auth_plugin() s = session.Session(auth=a) data = a.get_endpoint_data(session=s, service_type='compute', interface='admin') self.assertEqual(self.TEST_COMPUTE_ADMIN + '/v3', data.url) # We should have gotten the version from the URL self.assertEqual((3, 0), data.api_version) def test_get_all_version_data_all_interfaces(self): for interface in ('public', 'internal', 'admin'): # The version discovery dict will not have a project_id disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_nova_microversion( href=getattr(self.TEST_VOLUME.versions['v3'].discovery, interface), id='v3.0', status='CURRENT', min_version='3.0', version='3.20') # Adding a v2 version to a service named volumev3 is not # an error. The service itself is cinder and has more than # one major version. disc.add_nova_microversion( href=getattr(self.TEST_VOLUME.versions['v2'].discovery, interface), id='v2.0', status='SUPPORTED') self.stub_url( 'GET', [], base_url=getattr(self.TEST_VOLUME.unversioned, interface) + '/', json=disc) for url in ( self.TEST_COMPUTE_PUBLIC, self.TEST_COMPUTE_INTERNAL, self.TEST_COMPUTE_ADMIN): disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_microversion( href=url, id='v2') disc.add_microversion( href=url, id='v2.1', min_version='2.1', max_version='2.35') self.stub_url('GET', [], base_url=url, json=disc) a = self.create_auth_plugin() s = session.Session(auth=a) identity_endpoint = 'http://127.0.0.1:35357/{}/'.format(self.version) data = s.get_all_version_data(interface=None) self.assertEqual({ 'RegionOne': { 'admin': { 'block-storage': [{ 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'raw_status': 'SUPPORTED', 'status': 'SUPPORTED', 'url': 'https://block-storage.example.com/admin/v2', 'version': '2.0' }, { 'collection': None, 'max_microversion': '3.20', 'min_microversion': '3.0', 'next_min_version': None, 'not_before': None, 'raw_status': 'CURRENT', 'status': 'CURRENT', 'url': 'https://block-storage.example.com/admin/v3', 'version': '3.0' }], 'compute': [{ 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'raw_status': 'stable', 'status': 'CURRENT', 'url': 'https://compute.example.com/nova/admin', 'version': '2.0' }, { 'collection': None, 'max_microversion': '2.35', 'min_microversion': '2.1', 'next_min_version': None, 'not_before': None, 'raw_status': 'stable', 'status': 'CURRENT', 'url': 'https://compute.example.com/nova/admin', 'version': '2.1'}], 'identity': [{ 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'raw_status': None, 'status': 'CURRENT', 'url': identity_endpoint, 'version': self.discovery_version, }] }, 'internal': { 'baremetal': [{ 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'raw_status': None, 'status': 'CURRENT', 'url': 'https://baremetal.example.com/internal/', 'version': None }], 'block-storage': [{ 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'raw_status': 'SUPPORTED', 'status': 'SUPPORTED', 'url': 'https://block-storage.example.com/internal/v2', 'version': '2.0' }, { 'collection': None, 'max_microversion': '3.20', 'min_microversion': '3.0', 'next_min_version': None, 'not_before': None, 'raw_status': 'CURRENT', 'status': 'CURRENT', 'url': 'https://block-storage.example.com/internal/v3', 'version': '3.0' }], 'compute': [{ 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'raw_status': 'stable', 'status': 'CURRENT', 'url': 'https://compute.example.com/nova/internal', 'version': '2.0' }, { 'collection': None, 'max_microversion': '2.35', 'min_microversion': '2.1', 'next_min_version': None, 'not_before': None, 'raw_status': 'stable', 'status': 'CURRENT', 'url': 'https://compute.example.com/nova/internal', 'version': '2.1' }] }, 'public': { 'block-storage': [{ 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'raw_status': 'SUPPORTED', 'status': 'SUPPORTED', 'url': 'https://block-storage.example.com/public/v2', 'version': '2.0' }, { 'collection': None, 'max_microversion': '3.20', 'min_microversion': '3.0', 'next_min_version': None, 'not_before': None, 'raw_status': 'CURRENT', 'status': 'CURRENT', 'url': 'https://block-storage.example.com/public/v3', 'version': '3.0' }], 'compute': [{ 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'raw_status': 'stable', 'status': 'CURRENT', 'url': 'https://compute.example.com/nova/public', 'version': '2.0' }, { 'collection': None, 'max_microversion': '2.35', 'min_microversion': '2.1', 'next_min_version': None, 'not_before': None, 'raw_status': 'stable', 'status': 'CURRENT', 'url': 'https://compute.example.com/nova/public', 'version': '2.1', }] } } }, data) def test_get_all_version_data(self): cinder_disc = fixture.DiscoveryList(v2=False, v3=False) # The version discovery dict will not have a project_id cinder_disc.add_nova_microversion( href=self.TEST_VOLUME.versions['v3'].discovery.public, id='v3.0', status='CURRENT', min_version='3.0', version='3.20') # Adding a v2 version to a service named volumev3 is not # an error. The service itself is cinder and has more than # one major version. cinder_disc.add_nova_microversion( href=self.TEST_VOLUME.versions['v2'].discovery.public, id='v2.0', status='SUPPORTED') self.stub_url( 'GET', [], base_url=self.TEST_VOLUME.unversioned.public + '/', json=cinder_disc) nova_disc = fixture.DiscoveryList(v2=False, v3=False) nova_disc.add_microversion( href=self.TEST_COMPUTE_PUBLIC, id='v2') nova_disc.add_microversion( href=self.TEST_COMPUTE_PUBLIC, id='v2.1', min_version='2.1', max_version='2.35') self.stub_url( 'GET', [], base_url=self.TEST_COMPUTE_PUBLIC, json=nova_disc) a = self.create_auth_plugin() s = session.Session(auth=a) data = s.get_all_version_data(interface='public') self.assertEqual({ 'RegionOne': { 'public': { 'block-storage': [{ 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'raw_status': 'SUPPORTED', 'status': 'SUPPORTED', 'url': 'https://block-storage.example.com/public/v2', 'version': '2.0' }, { 'collection': None, 'max_microversion': '3.20', 'min_microversion': '3.0', 'next_min_version': None, 'not_before': None, 'raw_status': 'CURRENT', 'status': 'CURRENT', 'url': 'https://block-storage.example.com/public/v3', 'version': '3.0' }], 'compute': [{ 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'raw_status': 'stable', 'status': 'CURRENT', 'url': 'https://compute.example.com/nova/public', 'version': '2.0' }, { 'collection': None, 'max_microversion': '2.35', 'min_microversion': '2.1', 'next_min_version': None, 'not_before': None, 'raw_status': 'stable', 'status': 'CURRENT', 'url': 'https://compute.example.com/nova/public', 'version': '2.1' }], } } }, data) def test_get_all_version_data_by_service_type(self): nova_disc = fixture.DiscoveryList(v2=False, v3=False) nova_disc.add_microversion( href=self.TEST_COMPUTE_PUBLIC, id='v2') nova_disc.add_microversion( href=self.TEST_COMPUTE_PUBLIC, id='v2.1', min_version='2.1', max_version='2.35') self.stub_url( 'GET', [], base_url=self.TEST_COMPUTE_PUBLIC, json=nova_disc) a = self.create_auth_plugin() s = session.Session(auth=a) data = s.get_all_version_data( interface='public', service_type='compute') self.assertEqual({ 'RegionOne': { 'public': { 'compute': [{ 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'raw_status': 'stable', 'status': 'CURRENT', 'url': 'https://compute.example.com/nova/public', 'version': '2.0' }, { 'collection': None, 'max_microversion': '2.35', 'min_microversion': '2.1', 'next_min_version': None, 'not_before': None, 'raw_status': 'stable', 'status': 'CURRENT', 'url': 'https://compute.example.com/nova/public', 'version': '2.1' }], } } }, data) def test_get_all_version_data_adapter(self): nova_disc = fixture.DiscoveryList(v2=False, v3=False) nova_disc.add_microversion( href=self.TEST_COMPUTE_PUBLIC, id='v2') nova_disc.add_microversion( href=self.TEST_COMPUTE_PUBLIC, id='v2.1', min_version='2.1', max_version='2.35') self.stub_url( 'GET', [], base_url=self.TEST_COMPUTE_PUBLIC, json=nova_disc) s = session.Session(auth=self.create_auth_plugin()) a = adapter.Adapter(session=s, service_type='compute') data = a.get_all_version_data() self.assertEqual({ 'RegionOne': { 'public': { 'compute': [{ 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'raw_status': 'stable', 'status': 'CURRENT', 'url': 'https://compute.example.com/nova/public', 'version': '2.0' }, { 'collection': None, 'max_microversion': '2.35', 'min_microversion': '2.1', 'next_min_version': None, 'not_before': None, 'raw_status': 'stable', 'status': 'CURRENT', 'url': 'https://compute.example.com/nova/public', 'version': '2.1' }], } } }, data) def test_get_all_version_data_service_alias(self): cinder_disc = fixture.DiscoveryList(v2=False, v3=False) # The version discovery dict will not have a project_id cinder_disc.add_nova_microversion( href=self.TEST_VOLUME.versions['v3'].discovery.public, id='v3.0', status='CURRENT', min_version='3.0', version='3.20') # Adding a v2 version to a service named volumev3 is not # an error. The service itself is cinder and has more than # one major version. cinder_disc.add_nova_microversion( href=self.TEST_VOLUME.versions['v2'].discovery.public, id='v2.0', status='SUPPORTED') self.stub_url( 'GET', [], base_url=self.TEST_VOLUME.unversioned.public + '/', json=cinder_disc) a = self.create_auth_plugin() s = session.Session(auth=a) data = s.get_all_version_data( interface='public', service_type='block-store') self.assertEqual({ 'RegionOne': { 'public': { 'block-storage': [{ 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'raw_status': 'SUPPORTED', 'status': 'SUPPORTED', 'url': 'https://block-storage.example.com/public/v2', 'version': '2.0' }, { 'collection': None, 'max_microversion': '3.20', 'min_microversion': '3.0', 'next_min_version': None, 'not_before': None, 'raw_status': 'CURRENT', 'status': 'CURRENT', 'url': 'https://block-storage.example.com/public/v3', 'version': '3.0' }], } } }, data) def test_endpoint_data_no_version_no_discovery(self): a = self.create_auth_plugin() s = session.Session(auth=a) data = a.get_endpoint_data(session=s, service_type='compute', interface='admin', discover_versions=False) self.assertEqual(self.TEST_COMPUTE_ADMIN, data.url) # There's no version in the URL and no document - we have no idea self.assertIsNone(data.api_version) def test_endpoint_data_version_url_no_discovery(self): a = self.create_auth_plugin() s = session.Session(auth=a) data = a.get_endpoint_data(session=s, service_type='volumev3', interface='admin', discover_versions=False) self.assertEqual( self.TEST_VOLUME.versions['v3'].service.admin, data.url) # There's v3 in the URL self.assertEqual((3, 0), data.api_version) def test_endpoint_no_version(self): a = self.create_auth_plugin() s = session.Session(auth=a) data = a.get_endpoint(session=s, service_type='compute', interface='admin') self.assertEqual(self.TEST_COMPUTE_ADMIN, data) def test_endpoint_data_relative_version(self): # need to construct list this way for relative disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_v2('v2.0') disc.add_v3('v3') self.stub_url('GET', [], base_url=self.TEST_COMPUTE_ADMIN, json=disc) a = self.create_auth_plugin() s = session.Session(auth=a) data_v2 = a.get_endpoint_data(session=s, service_type='compute', interface='admin', min_version=(2, 0), max_version=(2, discover.LATEST)) data_v3 = a.get_endpoint_data(session=s, service_type='compute', interface='admin', min_version=(3, 0), max_version=(3, discover.LATEST)) self.assertEqual(self.TEST_COMPUTE_ADMIN + '/v2.0', data_v2.url) self.assertEqual(self.TEST_COMPUTE_ADMIN + '/v3', data_v3.url) def test_get_versioned_data(self): v2_compute = self.TEST_COMPUTE_ADMIN + '/v2.0' v3_compute = self.TEST_COMPUTE_ADMIN + '/v3' disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_v2(v2_compute) disc.add_v3(v3_compute) # Make sure that we don't do more than one discovery call # register responses such that if the discovery URL is hit more than # once then the response will be invalid and not point to COMPUTE_ADMIN resps = [{'json': disc}, {'status_code': 500}] self.requests_mock.get(self.TEST_COMPUTE_ADMIN, resps) a = self.create_auth_plugin() s = session.Session(auth=a) data = a.get_endpoint_data(session=s, service_type='compute', interface='admin') self.assertEqual(v3_compute, data.url) v2_data = data.get_versioned_data(s, min_version='2.0', max_version='2.latest') self.assertEqual(v2_compute, v2_data.url) self.assertEqual(v2_compute, v2_data.service_url) self.assertEqual(self.TEST_COMPUTE_ADMIN, v2_data.catalog_url) # Variants that all return v3 data for vkwargs in (dict(min_version='3.0', max_version='3.latest'), # min/max spans major versions dict(min_version='2.0', max_version='3.latest'), # latest major max dict(min_version='2.0', max_version='latest'), # implicit max dict(min_version='2.0'), # implicit min/max dict()): v3_data = data.get_versioned_data(s, **vkwargs) self.assertEqual(v3_compute, v3_data.url) self.assertEqual(v3_compute, v3_data.service_url) self.assertEqual(self.TEST_COMPUTE_ADMIN, v3_data.catalog_url) def test_get_current_versioned_data(self): v2_compute = self.TEST_COMPUTE_ADMIN + '/v2.0' v3_compute = self.TEST_COMPUTE_ADMIN + '/v3' disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_v2(v2_compute) disc.add_v3(v3_compute) # Make sure that we don't do more than one discovery call # register responses such that if the discovery URL is hit more than # once then the response will be invalid and not point to COMPUTE_ADMIN resps = [{'json': disc}, {'status_code': 500}] self.requests_mock.get(self.TEST_COMPUTE_ADMIN, resps) a = self.create_auth_plugin() s = session.Session(auth=a) data = a.get_endpoint_data(session=s, service_type='compute', interface='admin') self.assertEqual(v3_compute, data.url) v3_data = data.get_current_versioned_data(s) self.assertEqual(v3_compute, v3_data.url) self.assertEqual(v3_compute, v3_data.service_url) self.assertEqual(self.TEST_COMPUTE_ADMIN, v3_data.catalog_url) self.assertEqual((3, 0), v3_data.api_version) self.assertIsNone(v3_data.min_microversion) self.assertIsNone(v3_data.max_microversion) def test_interface_list(self): a = self.create_auth_plugin() s = session.Session(auth=a) ep = s.get_endpoint(service_type='baremetal', interface=['internal', 'public']) self.assertEqual(ep, self.TEST_BAREMETAL_INTERNAL) ep = s.get_endpoint(service_type='baremetal', interface=['public', 'internal']) self.assertEqual(ep, self.TEST_BAREMETAL_INTERNAL) ep = s.get_endpoint(service_type='compute', interface=['internal', 'public']) self.assertEqual(ep, self.TEST_COMPUTE_INTERNAL) ep = s.get_endpoint(service_type='compute', interface=['public', 'internal']) self.assertEqual(ep, self.TEST_COMPUTE_PUBLIC) def test_get_versioned_data_volume_project_id(self): disc = fixture.DiscoveryList(v2=False, v3=False) # The version discovery dict will not have a project_id disc.add_nova_microversion( href=self.TEST_VOLUME.versions['v3'].discovery.public, id='v3.0', status='CURRENT', min_version='3.0', version='3.20') # Adding a v2 version to a service named volumev3 is not # an error. The service itself is cinder and has more than # one major version. disc.add_nova_microversion( href=self.TEST_VOLUME.versions['v2'].discovery.public, id='v2.0', status='SUPPORTED') a = self.create_auth_plugin() s = session.Session(auth=a) # volume endpoint ends in v3, we should not make an API call endpoint = a.get_endpoint(session=s, service_type='volumev3', interface='public', version='3.0') self.assertEqual(self.TEST_VOLUME.catalog.public, endpoint) resps = [{'json': disc}, {'status_code': 500}] # We should only try to fetch the versioned discovery url once self.requests_mock.get( self.TEST_VOLUME.versions['v3'].discovery.public + '/', resps) data = a.get_endpoint_data(session=s, service_type='volumev3', interface='public') self.assertEqual(self.TEST_VOLUME.versions['v3'].service.public, data.url) v3_data = data.get_versioned_data( s, min_version='3.0', max_version='3.latest', project_id=self.project_id) self.assertEqual(self.TEST_VOLUME.versions['v3'].service.public, v3_data.url) self.assertEqual(self.TEST_VOLUME.catalog.public, v3_data.catalog_url) self.assertEqual((3, 0), v3_data.min_microversion) self.assertEqual((3, 20), v3_data.max_microversion) self.assertEqual(self.TEST_VOLUME.versions['v3'].service.public, v3_data.service_url) # Because of the v3 optimization before, requesting v2 should now go # find the unversioned endpoint self.requests_mock.get(self.TEST_VOLUME.unversioned.public, resps) v2_data = data.get_versioned_data( s, min_version='2.0', max_version='2.latest', project_id=self.project_id) # Even though we never requested volumev2 from the catalog, we should # wind up re-constructing it via version discovery and re-appending # the project_id to the URL self.assertEqual(self.TEST_VOLUME.versions['v2'].service.public, v2_data.url) self.assertEqual(self.TEST_VOLUME.versions['v2'].service.public, v2_data.service_url) self.assertEqual(self.TEST_VOLUME.catalog.public, v2_data.catalog_url) self.assertIsNone(v2_data.min_microversion) self.assertIsNone(v2_data.max_microversion) def test_get_versioned_data_volume_project_id_unversioned_first(self): disc = fixture.DiscoveryList(v2=False, v3=False) # The version discovery dict will not have a project_id disc.add_nova_microversion( href=self.TEST_VOLUME.versions['v3'].discovery.public, id='v3.0', status='CURRENT', min_version='3.0', version='3.20') # Adding a v2 version to a service named volumev3 is not # an error. The service itself is cinder and has more than # one major version. disc.add_nova_microversion( href=self.TEST_VOLUME.versions['v2'].discovery.public, id='v2.0', status='SUPPORTED') a = self.create_auth_plugin() s = session.Session(auth=a) # cinder endpoint ends in v3, we should not make an API call endpoint = a.get_endpoint(session=s, service_type='volumev3', interface='public', version='3.0') self.assertEqual(self.TEST_VOLUME.catalog.public, endpoint) resps = [{'json': disc}, {'status_code': 500}] # We should only try to fetch the unversioned non-project_id url once # Because the catalog has the versioned endpoint but we constructed # an unversioned endpoint, the url needs to have a trailing / self.requests_mock.get( self.TEST_VOLUME.unversioned.public + '/', resps) # Fetch v2.0 first - since that doesn't match endpoint optimization, # it should fetch the unversioned endpoint v2_data = s.get_endpoint_data(service_type='block-storage', interface='public', min_version='2.0', max_version='2.latest', project_id=self.project_id) # Even though we never requested volumev2 from the catalog, we should # wind up re-constructing it via version discovery and re-appending # the project_id to the URL self.assertEqual(self.TEST_VOLUME.versions['v2'].service.public, v2_data.url) self.assertEqual(self.TEST_VOLUME.versions['v2'].service.public, v2_data.service_url) self.assertEqual(self.TEST_VOLUME.catalog.public, v2_data.catalog_url) self.assertIsNone(v2_data.min_microversion) self.assertIsNone(v2_data.max_microversion) # Since we fetched from the unversioned endpoint to satisfy the # request for v2, we should have all the relevant data cached in the # discovery object - and should not fetch anything new. v3_data = v2_data.get_versioned_data( s, min_version='3.0', max_version='3.latest', project_id=self.project_id) self.assertEqual(self.TEST_VOLUME.versions['v3'].service.public, v3_data.url) self.assertEqual(self.TEST_VOLUME.catalog.public, v3_data.catalog_url) self.assertEqual((3, 0), v3_data.min_microversion) self.assertEqual((3, 20), v3_data.max_microversion) self.assertEqual(self.TEST_VOLUME.versions['v3'].service.public, v3_data.service_url) def test_trailing_slash_on_computed_endpoint(self): disc = fixture.DiscoveryList(v2=False, v3=False) # A versioned URL in the Catalog disc.add_nova_microversion( href=self.TEST_VOLUME.versions['v3'].discovery.public, id='v3.0', status='CURRENT', min_version='3.0', version='3.20') a = self.create_auth_plugin() s = session.Session(auth=a) # endpoint ends in v3, we will construct the unversioned endpoint. # Because the catalog has the versioned endpoint but we constructed # an unversioned endpoint, the url needs to have a trailing / self.requests_mock.get( self.TEST_VOLUME.unversioned.public + '/', json=disc) # We're requesting version 2 of block-storage to make sure we # trigger the logic constructing the unversioned endpoint from the # versioned endpoint in the catalog s.get_endpoint_data(service_type='block-storage', interface='public', min_version='2.0', max_version='2.latest', project_id=self.project_id) self.assertTrue( self.requests_mock.request_history[-1].url.endswith('/')) def test_no_trailing_slash_on_catalog_endpoint(self): disc = fixture.DiscoveryList(v2=False, v3=False) # A versioned URL in the Catalog disc.add_nova_microversion( href=self.TEST_COMPUTE_PUBLIC, id='v2.1', status='CURRENT', min_version='2.1', version='2.38') a = self.create_auth_plugin() s = session.Session(auth=a) # nova has unversioned endpoint in this catalog. We should not # modify it. self.requests_mock.get(self.TEST_COMPUTE_PUBLIC, json=disc) s.get_endpoint_data(service_type='compute', interface='public', min_version='2.1', max_version='2.latest') self.assertFalse( self.requests_mock.request_history[-1].url.endswith('/')) def test_broken_discovery_endpoint(self): # Discovery document with a bogus/broken base URL disc = fixture.DiscoveryList(v2=False, v3=False) disc.add_nova_microversion( href='http://internal.example.com', id='v2.1', status='CURRENT', min_version='2.1', version='2.38') a = self.create_auth_plugin() s = session.Session(auth=a) self.requests_mock.get(self.TEST_COMPUTE_PUBLIC, json=disc) data = s.get_endpoint_data(service_type='compute', interface='public', min_version='2.1', max_version='2.latest') # Verify that we get the versioned url based on the catalog url self.assertTrue(data.url, self.TEST_COMPUTE_PUBLIC + '/v2.1') def test_asking_for_auth_endpoint_ignores_checks(self): a = self.create_auth_plugin() s = session.Session(auth=a) auth_url = s.get_endpoint(service_type='compute', interface=plugin.AUTH_INTERFACE) self.assertEqual(self.TEST_URL, auth_url) def _create_expired_auth_plugin(self, **kwargs): expires = _utils.before_utcnow(minutes=20) expired_token = self.get_auth_data(expires=expires) expired_auth_ref = access.create(body=expired_token) a = self.create_auth_plugin(**kwargs) a.auth_ref = expired_auth_ref return a def test_reauthenticate(self): a = self._create_expired_auth_plugin() expired_auth_ref = a.auth_ref s = session.Session(auth=a) self.assertIsNot(expired_auth_ref, a.get_access(s)) def test_no_reauthenticate(self): a = self._create_expired_auth_plugin(reauthenticate=False) expired_auth_ref = a.auth_ref s = session.Session(auth=a) self.assertIs(expired_auth_ref, a.get_access(s)) def test_invalidate(self): a = self.create_auth_plugin() s = session.Session(auth=a) # trigger token fetching s.get_auth_headers() self.assertTrue(a.auth_ref) self.assertTrue(a.invalidate()) self.assertIsNone(a.auth_ref) self.assertFalse(a.invalidate()) def test_get_auth_properties(self): a = self.create_auth_plugin() s = session.Session() self.assertEqual(self.user_id, a.get_user_id(s)) self.assertEqual(self.project_id, a.get_project_id(s)) def assertAccessInfoEqual(self, a, b): self.assertEqual(a.auth_token, b.auth_token) self.assertEqual(a._data, b._data) def test_check_cache_id_match(self): a = self.create_auth_plugin() b = self.create_auth_plugin() self.assertIsNot(a, b) self.assertIsNone(a.get_auth_state()) self.assertIsNone(b.get_auth_state()) a_id = a.get_cache_id() b_id = b.get_cache_id() self.assertIsNotNone(a_id) self.assertIsNotNone(b_id) self.assertEqual(a_id, b_id) def test_check_cache_id_no_match(self): a = self.create_auth_plugin(project_id='a') b = self.create_auth_plugin(project_id='b') self.assertIsNot(a, b) self.assertIsNone(a.get_auth_state()) self.assertIsNone(b.get_auth_state()) a_id = a.get_cache_id() b_id = b.get_cache_id() self.assertIsNotNone(a_id) self.assertIsNotNone(b_id) self.assertNotEqual(a_id, b_id) def test_get_set_auth_state(self): a = self.create_auth_plugin() b = self.create_auth_plugin() self.assertEqual(a.get_cache_id(), b.get_cache_id()) s = session.Session() a_token = a.get_token(s) self.assertEqual(1, self.requests_mock.call_count) auth_state = a.get_auth_state() self.assertIsNotNone(auth_state) b.set_auth_state(auth_state) b_token = b.get_token(s) self.assertEqual(1, self.requests_mock.call_count) self.assertEqual(a_token, b_token) self.assertAccessInfoEqual(a.auth_ref, b.auth_ref) def test_pathless_url(self): disc = fixture.DiscoveryList(v2=False, v3=False) url = 'http://path.less.url:1234' disc.add_microversion(href=url, id='v2.1') self.stub_url('GET', base_url=url, status_code=200, json=disc) token = fixture.V2Token() service = token.add_service('network') service.add_endpoint(public=url, admin=url, internal=url) self.stub_url('POST', ['tokens'], base_url=url, json=token) v2_auth = identity.V2Password(url, username='u', password='p') sess = session.Session(auth=v2_auth) data = sess.get_endpoint_data(service_type='network') # Discovery ran and returned the URL and its version self.assertEqual(url, data.url) self.assertEqual((2, 1), data.api_version) # Run with a project_id to ensure that path is covered self.assertEqual( 3, len(list(data._get_discovery_url_choices(project_id='42')))) class V3(CommonIdentityTests, utils.TestCase): @property def version(self): return 'v3' @property def discovery_version(self): return '3.0' def get_auth_data(self, **kwargs): kwargs.setdefault('project_id', self.PROJECT_ID) token = fixture.V3Token(**kwargs) region = 'RegionOne' svc = token.add_service('identity') svc.add_standard_endpoints(admin=self.TEST_ADMIN_URL, region=region) svc = token.add_service('compute') svc.add_standard_endpoints(admin=self.TEST_COMPUTE_ADMIN, public=self.TEST_COMPUTE_PUBLIC, internal=self.TEST_COMPUTE_INTERNAL, region=region) svc = token.add_service('volumev2') svc.add_standard_endpoints( admin=self.TEST_VOLUME.versions['v2'].service.admin, public=self.TEST_VOLUME.versions['v2'].service.public, internal=self.TEST_VOLUME.versions['v2'].service.internal, region=region) svc = token.add_service('volumev3') svc.add_standard_endpoints( admin=self.TEST_VOLUME.versions['v3'].service.admin, public=self.TEST_VOLUME.versions['v3'].service.public, internal=self.TEST_VOLUME.versions['v3'].service.internal, region=region) # Add block-storage as a versioned endpoint so that we can test # versioned to unversioned inference. svc = token.add_service('block-storage') svc.add_standard_endpoints( admin=self.TEST_VOLUME.versions['v3'].service.admin, public=self.TEST_VOLUME.versions['v3'].service.public, internal=self.TEST_VOLUME.versions['v3'].service.internal, region=region) svc = token.add_service('baremetal') svc.add_standard_endpoints( internal=self.TEST_BAREMETAL_INTERNAL, region=region) return token def stub_auth(self, subject_token=None, **kwargs): if not subject_token: subject_token = self.TEST_TOKEN kwargs.setdefault('headers', {})['X-Subject-Token'] = subject_token self.stub_url('POST', ['auth', 'tokens'], **kwargs) def create_auth_plugin(self, **kwargs): kwargs.setdefault('auth_url', self.TEST_URL) kwargs.setdefault('username', self.TEST_USER) kwargs.setdefault('password', self.TEST_PASS) return identity.V3Password(**kwargs) class V2(CommonIdentityTests, utils.TestCase): @property def version(self): return 'v2.0' @property def discovery_version(self): return '2.0' def create_auth_plugin(self, **kwargs): kwargs.setdefault('auth_url', self.TEST_URL) kwargs.setdefault('username', self.TEST_USER) kwargs.setdefault('password', self.TEST_PASS) try: kwargs.setdefault('tenant_id', kwargs.pop('project_id')) except KeyError: pass try: kwargs.setdefault('tenant_name', kwargs.pop('project_name')) except KeyError: pass return identity.V2Password(**kwargs) def get_auth_data(self, **kwargs): kwargs.setdefault('tenant_id', self.PROJECT_ID) token = fixture.V2Token(**kwargs) region = 'RegionOne' svc = token.add_service('identity') svc.add_endpoint(admin=self.TEST_ADMIN_URL, region=region, public=None, internal=None) svc = token.add_service('compute') svc.add_endpoint(public=self.TEST_COMPUTE_PUBLIC, internal=self.TEST_COMPUTE_INTERNAL, admin=self.TEST_COMPUTE_ADMIN, region=region) svc = token.add_service('volumev2') svc.add_endpoint( admin=self.TEST_VOLUME.versions['v2'].service.admin, public=self.TEST_VOLUME.versions['v2'].service.public, internal=self.TEST_VOLUME.versions['v2'].service.internal, region=region) svc = token.add_service('volumev3') svc.add_endpoint( admin=self.TEST_VOLUME.versions['v3'].service.admin, public=self.TEST_VOLUME.versions['v3'].service.public, internal=self.TEST_VOLUME.versions['v3'].service.internal, region=region) # Add block-storage as a versioned endpoint so that we can test # versioned to unversioned inferance. svc = token.add_service('block-storage') svc.add_endpoint( admin=self.TEST_VOLUME.versions['v3'].service.admin, public=self.TEST_VOLUME.versions['v3'].service.public, internal=self.TEST_VOLUME.versions['v3'].service.internal, region=region) svc = token.add_service('baremetal') svc.add_endpoint( public=None, admin=None, internal=self.TEST_BAREMETAL_INTERNAL, region=region) return token def stub_auth(self, **kwargs): self.stub_url('POST', ['tokens'], **kwargs) class CatalogHackTests(utils.TestCase): TEST_URL = 'http://keystone.server:5000/v2.0' OTHER_URL = 'http://other.server:5000/path' IDENTITY = 'identity' BASE_URL = 'http://keystone.server:5000/' V2_URL = BASE_URL + 'v2.0' V3_URL = BASE_URL + 'v3' PROJECT_ID = uuid.uuid4().hex def test_getting_endpoints(self): disc = fixture.DiscoveryList(href=self.BASE_URL) self.stub_url('GET', ['/'], base_url=self.BASE_URL, json=disc) token = fixture.V2Token() service = token.add_service(self.IDENTITY) service.add_endpoint(public=self.V2_URL, admin=self.V2_URL, internal=self.V2_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) endpoint = sess.get_endpoint(service_type=self.IDENTITY, interface='public', version=(3, 0)) self.assertEqual(self.V3_URL, endpoint) def test_returns_original_when_discover_fails(self): token = fixture.V2Token() service = token.add_service(self.IDENTITY) service.add_endpoint(public=self.V2_URL, admin=self.V2_URL, internal=self.V2_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) self.stub_url('GET', [], base_url=self.BASE_URL, status_code=404) self.stub_url('GET', [], base_url=self.V2_URL, status_code=404) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) endpoint = sess.get_endpoint(service_type=self.IDENTITY, interface='public', version=(3, 0)) self.assertEqual(self.V2_URL, endpoint) def test_getting_endpoints_project_id_and_trailing_slash_in_disc_url(self): # Test that when requesting a v3 endpoint and having a project in the # session but only the v2 endpoint with a trailing slash in the # catalog, we can still discover the v3 endpoint. disc = fixture.DiscoveryList(href=self.BASE_URL) self.stub_url('GET', ['/'], base_url=self.BASE_URL, json=disc) # Create a project-scoped token. This will exercise the flow in the # discovery URL sequence where a project ID exists in the token but # there is no project ID in the URL. token = fixture.V3Token(project_id=self.PROJECT_ID) # Add only a v2 endpoint with a trailing slash service = token.add_service(self.IDENTITY) service.add_endpoint('public', self.V2_URL + '/') service.add_endpoint('admin', self.V2_URL + '/') # Auth with v3 kwargs = {'headers': {'X-Subject-Token': self.TEST_TOKEN}} self.stub_url('POST', ['auth', 'tokens'], base_url=self.V3_URL, json=token, **kwargs) v3_auth = identity.V3Password(self.V3_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v3_auth) # Try to get a v3 endpoint endpoint = sess.get_endpoint(service_type=self.IDENTITY, interface='public', version=(3, 0)) self.assertEqual(self.V3_URL, endpoint) def test_returns_original_skipping_discovery(self): token = fixture.V2Token() service = token.add_service(self.IDENTITY) service.add_endpoint(public=self.V2_URL, admin=self.V2_URL, internal=self.V2_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) endpoint = sess.get_endpoint(service_type=self.IDENTITY, interface='public', skip_discovery=True, version=(3, 0)) self.assertEqual(self.V2_URL, endpoint) def test_endpoint_override_skips_discovery(self): token = fixture.V2Token() service = token.add_service(self.IDENTITY) service.add_endpoint(public=self.V2_URL, admin=self.V2_URL, internal=self.V2_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) endpoint = sess.get_endpoint(endpoint_override=self.OTHER_URL, service_type=self.IDENTITY, interface='public', version=(3, 0)) self.assertEqual(self.OTHER_URL, endpoint) def test_endpoint_override_data_runs_discovery(self): common_disc = fixture.DiscoveryList(v2=False, v3=False) common_disc.add_microversion(href=self.OTHER_URL, id='v2.1', min_version='2.1', max_version='2.35') common_m = self.stub_url('GET', base_url=self.OTHER_URL, status_code=200, json=common_disc) token = fixture.V2Token() service = token.add_service(self.IDENTITY) service.add_endpoint(public=self.V2_URL, admin=self.V2_URL, internal=self.V2_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) data = sess.get_endpoint_data(endpoint_override=self.OTHER_URL, service_type=self.IDENTITY, interface='public', min_version=(2, 0), max_version=(2, discover.LATEST)) self.assertTrue(common_m.called) self.assertEqual(self.OTHER_URL, data.service_url) self.assertEqual(self.OTHER_URL, data.catalog_url) self.assertEqual(self.OTHER_URL, data.url) self.assertEqual((2, 1), data.min_microversion) self.assertEqual((2, 35), data.max_microversion) self.assertEqual((2, 1), data.api_version) def test_forcing_discovery(self): v2_disc = fixture.V2Discovery(self.V2_URL) common_disc = fixture.DiscoveryList(href=self.BASE_URL) v2_m = self.stub_url('GET', ['v2.0'], base_url=self.BASE_URL, status_code=200, json={'version': v2_disc}) common_m = self.stub_url('GET', [], base_url=self.BASE_URL, status_code=300, json=common_disc) token = fixture.V2Token() service = token.add_service(self.IDENTITY) service.add_endpoint(public=self.V2_URL, admin=self.V2_URL, internal=self.V2_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) # v2 auth with v2 url doesn't make any discovery calls. self.assertFalse(v2_m.called) self.assertFalse(common_m.called) data = sess.get_endpoint_data(service_type=self.IDENTITY, discover_versions=True) # We should get the v2 document, but not the unversioned self.assertTrue(v2_m.called) self.assertFalse(common_m.called) # got v2 url self.assertEqual(self.V2_URL, data.url) self.assertEqual((2, 0), data.api_version) def test_forcing_discovery_list_returns_url(self): common_disc = fixture.DiscoveryList(href=self.BASE_URL) # 2.0 doesn't usually return a list. This is testing that if # the catalog url returns an endpoint that has a discovery document # with more than one URL and that a different url would be returned # by "return the latest" rules, that we get the info of the url from # the catalog if we don't provide a version but do provide # discover_versions v2_m = self.stub_url('GET', ['v2.0'], base_url=self.BASE_URL, status_code=200, json=common_disc) token = fixture.V2Token() service = token.add_service(self.IDENTITY) service.add_endpoint(public=self.V2_URL, admin=self.V2_URL, internal=self.V2_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) # v2 auth with v2 url doesn't make any discovery calls. self.assertFalse(v2_m.called) data = sess.get_endpoint_data(service_type=self.IDENTITY, discover_versions=True) # We should make the one call self.assertTrue(v2_m.called) # got v2 url self.assertEqual(self.V2_URL, data.url) self.assertEqual((2, 0), data.api_version) def test_latest_version_gets_latest_version(self): common_disc = fixture.DiscoveryList(href=self.BASE_URL) # 2.0 doesn't usually return a list. But we're testing version matching # rules, so it's nice to ensure that we don't fallback to something v2_m = self.stub_url('GET', base_url=self.BASE_URL, status_code=200, json=common_disc) token = fixture.V2Token() service = token.add_service(self.IDENTITY) service.add_endpoint(public=self.V2_URL, admin=self.V2_URL, internal=self.V2_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) # v2 auth with v2 url doesn't make any discovery calls. self.assertFalse(v2_m.called) endpoint = sess.get_endpoint(service_type=self.IDENTITY, version='latest') # We should make the one call self.assertTrue(v2_m.called) # And get the v3 url self.assertEqual(self.V3_URL, endpoint) # Make sure latest logic works for min and max version endpoint = sess.get_endpoint(service_type=self.IDENTITY, max_version='latest') self.assertEqual(self.V3_URL, endpoint) endpoint = sess.get_endpoint(service_type=self.IDENTITY, min_version='latest') self.assertEqual(self.V3_URL, endpoint) endpoint = sess.get_endpoint(service_type=self.IDENTITY, min_version='latest', max_version='latest') self.assertEqual(self.V3_URL, endpoint) self.assertRaises(ValueError, sess.get_endpoint, service_type=self.IDENTITY, min_version='latest', max_version='3.0') def test_version_range(self): v2_disc = fixture.V2Discovery(self.V2_URL) common_disc = fixture.DiscoveryList(href=self.BASE_URL) def stub_urls(): v2_m = self.stub_url('GET', ['v2.0'], base_url=self.BASE_URL, status_code=200, json={'version': v2_disc}) common_m = self.stub_url('GET', base_url=self.BASE_URL, status_code=200, json=common_disc) return v2_m, common_m v2_m, common_m = stub_urls() token = fixture.V2Token() service = token.add_service(self.IDENTITY) service.add_endpoint(public=self.V2_URL, admin=self.V2_URL, internal=self.V2_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) # v2 auth with v2 url doesn't make any discovery calls. self.assertFalse(v2_m.called) endpoint = sess.get_endpoint(service_type=self.IDENTITY, min_version='2.0', max_version='3.0') # We should make the one call self.assertFalse(v2_m.called) self.assertTrue(common_m.called) # And get the v3 url self.assertEqual(self.V3_URL, endpoint) v2_m, common_m = stub_urls() endpoint = sess.get_endpoint(service_type=self.IDENTITY, min_version='1', max_version='2') # We should make no calls - we peek in the cache self.assertFalse(v2_m.called) self.assertFalse(common_m.called) # And get the v2 url self.assertEqual(self.V2_URL, endpoint) v2_m, common_m = stub_urls() endpoint = sess.get_endpoint(service_type=self.IDENTITY, min_version='4') # We should make no more calls self.assertFalse(v2_m.called) self.assertFalse(common_m.called) # And get no url self.assertIsNone(endpoint) v2_m, common_m = stub_urls() endpoint = sess.get_endpoint(service_type=self.IDENTITY, min_version='2') # We should make no more calls self.assertFalse(v2_m.called) self.assertFalse(common_m.called) # And get the v3 url self.assertEqual(self.V3_URL, endpoint) v2_m, common_m = stub_urls() self.assertRaises(ValueError, sess.get_endpoint, service_type=self.IDENTITY, version=3, min_version='2') # We should make no more calls self.assertFalse(v2_m.called) self.assertFalse(common_m.called) def test_get_endpoint_data(self): common_disc = fixture.DiscoveryList(v2=False, v3=False) common_disc.add_microversion(href=self.OTHER_URL, id='v2.1', min_version='2.1', max_version='2.35') common_m = self.stub_url('GET', base_url=self.OTHER_URL, status_code=200, json=common_disc) token = fixture.V2Token() service = token.add_service('network') service.add_endpoint(public=self.OTHER_URL, admin=self.OTHER_URL, internal=self.OTHER_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) # v2 auth with v2 url doesn't make any discovery calls. self.assertFalse(common_m.called) data = sess.get_endpoint_data(service_type='network', min_version='2.0', max_version='3.0') # We should make the one call self.assertTrue(common_m.called) # And get the v3 url self.assertEqual(self.OTHER_URL, data.url) self.assertEqual((2, 1), data.min_microversion) self.assertEqual((2, 35), data.max_microversion) self.assertEqual((2, 1), data.api_version) def test_get_endpoint_data_compute(self): common_disc = fixture.DiscoveryList(v2=False, v3=False) common_disc.add_nova_microversion(href=self.OTHER_URL, id='v2.1', min_version='2.1', version='2.35') common_m = self.stub_url('GET', base_url=self.OTHER_URL, status_code=200, json=common_disc) token = fixture.V2Token() service = token.add_service('compute') service.add_endpoint(public=self.OTHER_URL, admin=self.OTHER_URL, internal=self.OTHER_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) # v2 auth with v2 url doesn't make any discovery calls. self.assertFalse(common_m.called) data = sess.get_endpoint_data(service_type='compute', min_version='2.0', max_version='3.0') # We should make the one call self.assertTrue(common_m.called) # And get the v3 url self.assertEqual(self.OTHER_URL, data.url) self.assertEqual((2, 1), data.min_microversion) self.assertEqual((2, 35), data.max_microversion) self.assertEqual((2, 1), data.api_version) def test_getting_endpoints_on_auth_interface(self): disc = fixture.DiscoveryList(href=self.BASE_URL) self.stub_url('GET', ['/'], base_url=self.BASE_URL, status_code=300, json=disc) token = fixture.V2Token() service = token.add_service(self.IDENTITY) service.add_endpoint(public=self.V2_URL, admin=self.V2_URL, internal=self.V2_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) endpoint = sess.get_endpoint(interface=plugin.AUTH_INTERFACE, version=(3, 0)) self.assertEqual(self.V3_URL, endpoint) def test_setting_no_discover_hack(self): v2_disc = fixture.V2Discovery(self.V2_URL) common_disc = fixture.DiscoveryList(href=self.BASE_URL) v2_m = self.stub_url('GET', ['v2.0'], base_url=self.BASE_URL, status_code=200, json=v2_disc) common_m = self.stub_url('GET', [], base_url=self.BASE_URL, status_code=300, json=common_disc) resp_text = uuid.uuid4().hex resp_m = self.stub_url('GET', ['v3', 'path'], base_url=self.BASE_URL, status_code=200, text=resp_text) # it doesn't matter that we auth with v2 here, discovery hack is in # base. All identity endpoints point to v2 urls. token = fixture.V2Token() service = token.add_service(self.IDENTITY) service.add_endpoint(public=self.V2_URL, admin=self.V2_URL, internal=self.V2_URL) self.stub_url('POST', ['tokens'], base_url=self.V2_URL, json=token) v2_auth = identity.V2Password(self.V2_URL, username=uuid.uuid4().hex, password=uuid.uuid4().hex) sess = session.Session(auth=v2_auth) # v2 auth with v2 url doesn't make any discovery calls. self.assertFalse(v2_m.called) self.assertFalse(common_m.called) # v3 endpoint with hack will strip v2 suffix and call root discovery endpoint = sess.get_endpoint(service_type=self.IDENTITY, version=(3, 0), allow_version_hack=True) # got v3 url self.assertEqual(self.V3_URL, endpoint) # only called root discovery. self.assertFalse(v2_m.called) self.assertTrue(common_m.called_once) # with hack turned off it calls v2 discovery and finds nothing endpoint = sess.get_endpoint(service_type=self.IDENTITY, version=(3, 0), allow_version_hack=False) self.assertIsNone(endpoint) # this one called v2 self.assertTrue(v2_m.called_once) self.assertTrue(common_m.called_once) # get_endpoint returning None raises EndpointNotFound when requesting self.assertRaises(exceptions.EndpointNotFound, sess.get, '/path', endpoint_filter={'service_type': 'identity', 'version': (3, 0), 'allow_version_hack': False}) self.assertFalse(resp_m.called) # works when allow_version_hack is set resp = sess.get('/path', endpoint_filter={'service_type': 'identity', 'version': (3, 0), 'allow_version_hack': True}) self.assertTrue(resp_m.called_once) self.assertEqual(resp_text, resp.text) class GenericPlugin(plugin.BaseAuthPlugin): BAD_TOKEN = uuid.uuid4().hex def __init__(self): super(GenericPlugin, self).__init__() self.endpoint = 'http://keystone.host:5000' self.headers = {'headerA': 'valueA', 'headerB': 'valueB'} self.cert = '/path/to/cert' self.connection_params = {'cert': self.cert, 'verify': False} def url(self, prefix): return '%s/%s' % (self.endpoint, prefix) def get_token(self, session, **kwargs): # NOTE(jamielennox): by specifying get_headers this should not be used return self.BAD_TOKEN def get_headers(self, session, **kwargs): return self.headers def get_endpoint(self, session, **kwargs): return self.endpoint def get_connection_params(self, session, **kwargs): return self.connection_params class GenericAuthPluginTests(utils.TestCase): # filter doesn't matter to GenericPlugin, but we have to specify one ENDPOINT_FILTER = {uuid.uuid4().hex: uuid.uuid4().hex} def setUp(self): super(GenericAuthPluginTests, self).setUp() self.auth = GenericPlugin() self.session = session.Session(auth=self.auth) def test_setting_headers(self): text = uuid.uuid4().hex self.stub_url('GET', base_url=self.auth.url('prefix'), text=text) resp = self.session.get('prefix', endpoint_filter=self.ENDPOINT_FILTER) self.assertEqual(text, resp.text) for k, v in self.auth.headers.items(): self.assertRequestHeaderEqual(k, v) self.assertIsNone(self.session.get_token()) self.assertEqual(self.auth.headers, self.session.get_auth_headers()) self.assertNotIn('X-Auth-Token', self.requests_mock.last_request.headers) def test_setting_connection_params(self): text = uuid.uuid4().hex self.stub_url('GET', base_url=self.auth.url('prefix'), text=text) resp = self.session.get('prefix', endpoint_filter=self.ENDPOINT_FILTER) self.assertEqual(text, resp.text) # the cert and verify values passed to request are those that were # returned from the auth plugin as connection params. self.assertEqual(self.auth.cert, self.requests_mock.last_request.cert) self.assertFalse(self.requests_mock.last_request.verify) def test_setting_bad_connection_params(self): # The uuid name parameter here is unknown and not in the allowed params # to be returned to the session and so an error will be raised. name = uuid.uuid4().hex self.auth.connection_params[name] = uuid.uuid4().hex e = self.assertRaises(exceptions.UnsupportedParameters, self.session.get, 'prefix', endpoint_filter=self.ENDPOINT_FILTER) self.assertIn(name, str(e)) class DiscoveryFailures(utils.TestCase): TEST_ROOT_URL = 'http://127.0.0.1:5000/' def test_connection_error(self): self.requests_mock.get(self.TEST_ROOT_URL, exc=exceptions.ConnectionError) sess = session.Session() p = identity.generic.password.Password(self.TEST_ROOT_URL) self.assertRaises(exceptions.DiscoveryFailure, p.get_auth_ref, sess) def test_client_exception(self): self.requests_mock.get(self.TEST_ROOT_URL, exc=exceptions.ClientException) sess = session.Session() p = identity.generic.password.Password(self.TEST_ROOT_URL) self.assertRaises(exceptions.ClientException, p.get_auth_ref, sess) def test_ssl_error(self): self.requests_mock.get(self.TEST_ROOT_URL, exc=exceptions.SSLError) sess = session.Session() p = identity.generic.password.Password(self.TEST_ROOT_URL) self.assertRaises(exceptions.DiscoveryFailure, p.get_auth_ref, sess) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/identity/test_identity_v2.py0000664000175000017500000003421000000000000026775 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import json import time import uuid from keystoneauth1 import _utils as ksa_utils from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1.identity import v2 from keystoneauth1 import session from keystoneauth1.tests.unit import utils class V2IdentityPlugin(utils.TestCase): TEST_ROOT_URL = 'http://127.0.0.1:5000/' TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v2.0') TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v2.0') TEST_PASS = 'password' TEST_SERVICE_CATALOG = [{ "endpoints": [{ "adminURL": "http://cdn.admin-nets.local:8774/v1.0", "region": "RegionOne", "internalURL": "http://127.0.0.1:8774/v1.0", "publicURL": "http://cdn.admin-nets.local:8774/v1.0/" }], "type": "nova_compat", "name": "nova_compat" }, { "endpoints": [{ "adminURL": "http://nova/novapi/admin", "region": "RegionOne", "internalURL": "http://nova/novapi/internal", "publicURL": "http://nova/novapi/public" }], "type": "compute", "name": "nova" }, { "endpoints": [{ "adminURL": "http://glance/glanceapi/admin", "region": "RegionOne", "internalURL": "http://glance/glanceapi/internal", "publicURL": "http://glance/glanceapi/public" }], "type": "image", "name": "glance" }, { "endpoints": [{ "adminURL": TEST_ADMIN_URL, "region": "RegionOne", "internalURL": "http://127.0.0.1:5000/v2.0", "publicURL": "http://127.0.0.1:5000/v2.0" }], "type": "identity", "name": "keystone" }, { "endpoints": [{ "adminURL": "http://swift/swiftapi/admin", "region": "RegionOne", "internalURL": "http://swift/swiftapi/internal", "publicURL": "http://swift/swiftapi/public" }], "type": "object-store", "name": "swift" }] def setUp(self): super(V2IdentityPlugin, self).setUp() self.TEST_RESPONSE_DICT = { "access": { "token": { "expires": "%i-02-01T00:00:10.000123Z" % (1 + time.gmtime().tm_year), "id": self.TEST_TOKEN, "tenant": { "id": self.TEST_TENANT_ID }, }, "user": { "id": self.TEST_USER }, "serviceCatalog": self.TEST_SERVICE_CATALOG, }, } def stub_auth(self, **kwargs): self.stub_url('POST', ['tokens'], **kwargs) def test_authenticate_with_username_password(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) self.assertIsNone(a.user_id) self.assertFalse(a.has_scope_parameters) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'passwordCredentials': {'username': self.TEST_USER, 'password': self.TEST_PASS}}} self.assertRequestBodyIs(json=req) self.assertRequestHeaderEqual('Content-Type', 'application/json') self.assertRequestHeaderEqual('Accept', 'application/json') self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_authenticate_with_user_id_password(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v2.Password(self.TEST_URL, user_id=self.TEST_USER, password=self.TEST_PASS) self.assertIsNone(a.username) self.assertFalse(a.has_scope_parameters) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'passwordCredentials': {'userId': self.TEST_USER, 'password': self.TEST_PASS}}} self.assertRequestBodyIs(json=req) self.assertRequestHeaderEqual('Content-Type', 'application/json') self.assertRequestHeaderEqual('Accept', 'application/json') self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_authenticate_with_username_password_scoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, tenant_id=self.TEST_TENANT_ID) self.assertTrue(a.has_scope_parameters) self.assertIsNone(a.user_id) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'passwordCredentials': {'username': self.TEST_USER, 'password': self.TEST_PASS}, 'tenantId': self.TEST_TENANT_ID}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_authenticate_with_user_id_password_scoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v2.Password(self.TEST_URL, user_id=self.TEST_USER, password=self.TEST_PASS, tenant_id=self.TEST_TENANT_ID) self.assertIsNone(a.username) self.assertTrue(a.has_scope_parameters) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'passwordCredentials': {'userId': self.TEST_USER, 'password': self.TEST_PASS}, 'tenantId': self.TEST_TENANT_ID}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_authenticate_with_token(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v2.Token(self.TEST_URL, 'foo') s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'token': {'id': 'foo'}}} self.assertRequestBodyIs(json=req) self.assertRequestHeaderEqual('x-Auth-Token', 'foo') self.assertRequestHeaderEqual('Content-Type', 'application/json') self.assertRequestHeaderEqual('Accept', 'application/json') self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_with_trust_id(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, trust_id='trust') self.assertTrue(a.has_scope_parameters) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'passwordCredentials': {'username': self.TEST_USER, 'password': self.TEST_PASS}, 'trust_id': 'trust'}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def _do_service_url_test(self, base_url, endpoint_filter): self.stub_auth(json=self.TEST_RESPONSE_DICT) self.stub_url('GET', ['path'], base_url=base_url, text='SUCCESS', status_code=200) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) resp = s.get('/path', endpoint_filter=endpoint_filter) self.assertEqual(resp.status_code, 200) self.assertEqual(self.requests_mock.last_request.url, base_url + '/path') def test_service_url(self): endpoint_filter = {'service_type': 'compute', 'interface': 'admin', 'service_name': 'nova'} self._do_service_url_test('http://nova/novapi/admin', endpoint_filter) def test_service_url_defaults_to_public(self): endpoint_filter = {'service_type': 'compute'} self._do_service_url_test('http://nova/novapi/public', endpoint_filter) def test_endpoint_filter_without_service_type_fails(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertRaises(exceptions.EndpointNotFound, s.get, '/path', endpoint_filter={'interface': 'admin'}) def test_full_url_overrides_endpoint_filter(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) self.stub_url('GET', [], base_url='http://testurl/', text='SUCCESS', status_code=200) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) resp = s.get('http://testurl/', endpoint_filter={'service_type': 'compute'}) self.assertEqual(resp.status_code, 200) self.assertEqual(resp.text, 'SUCCESS') def test_invalid_auth_response_dict(self): self.stub_auth(json={'hello': 'world'}) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', authenticated=True) def test_invalid_auth_response_type(self): self.stub_url('POST', ['tokens'], text='testdata') a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', authenticated=True) def test_invalidate_response(self): resp_data1 = copy.deepcopy(self.TEST_RESPONSE_DICT) resp_data2 = copy.deepcopy(self.TEST_RESPONSE_DICT) resp_data1['access']['token']['id'] = 'token1' resp_data2['access']['token']['id'] = 'token2' auth_responses = [{'json': resp_data1}, {'json': resp_data2}] self.stub_auth(response_list=auth_responses) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertEqual('token1', s.get_token()) self.assertEqual({'X-Auth-Token': 'token1'}, s.get_auth_headers()) a.invalidate() self.assertEqual('token2', s.get_token()) self.assertEqual({'X-Auth-Token': 'token2'}, s.get_auth_headers()) def test_doesnt_log_password(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) password = uuid.uuid4().hex a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=password) s = session.Session(auth=a) self.assertEqual(self.TEST_TOKEN, s.get_token()) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) self.assertNotIn(password, self.logger.output) def test_password_with_no_user_id_or_name(self): self.assertRaises(TypeError, v2.Password, self.TEST_URL, password=self.TEST_PASS) def test_password_cache_id(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) trust_id = uuid.uuid4().hex a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, trust_id=trust_id) b = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, trust_id=trust_id) a_id = a.get_cache_id() b_id = b.get_cache_id() self.assertEqual(a_id, b_id) c = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, tenant_id=trust_id) # same value different param c_id = c.get_cache_id() self.assertNotEqual(a_id, c_id) self.assertIsNone(a.get_auth_state()) self.assertIsNone(b.get_auth_state()) self.assertIsNone(c.get_auth_state()) s = session.Session() self.assertEqual(self.TEST_TOKEN, a.get_token(s)) self.assertTrue(self.requests_mock.called) def test_password_change_auth_state(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) expired = ksa_utils.before_utcnow(days=2) token = fixture.V2Token(expires=expired) auth_ref = access.create(body=token) a = v2.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, tenant_id=uuid.uuid4().hex) initial_cache_id = a.get_cache_id() state = a.get_auth_state() self.assertIsNone(state) state = json.dumps({'auth_token': auth_ref.auth_token, 'body': auth_ref._data}) a.set_auth_state(state) self.assertEqual(token.token_id, a.auth_ref.auth_token) s = session.Session() self.assertEqual(self.TEST_TOKEN, a.get_token(s)) # updates expired self.assertEqual(initial_cache_id, a.get_cache_id()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/identity/test_identity_v3.py0000664000175000017500000007564200000000000027014 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import json import time import uuid from keystoneauth1 import _utils as ksa_utils from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1.identity import v3 from keystoneauth1.identity.v3 import base as v3_base from keystoneauth1 import session from keystoneauth1.tests.unit import utils class V3IdentityPlugin(utils.TestCase): TEST_ROOT_URL = 'http://127.0.0.1:5000/' TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v3') TEST_PASS = 'password' TEST_APP_CRED_ID = 'appcredid' TEST_APP_CRED_SECRET = 'secret' TEST_SERVICE_CATALOG = [{ "endpoints": [{ "url": "http://cdn.admin-nets.local:8774/v1.0/", "region": "RegionOne", "interface": "public" }, { "url": "http://127.0.0.1:8774/v1.0", "region": "RegionOne", "interface": "internal" }, { "url": "http://cdn.admin-nets.local:8774/v1.0", "region": "RegionOne", "interface": "admin" }], "type": "nova_compat" }, { "endpoints": [{ "url": "http://nova/novapi/public", "region": "RegionOne", "interface": "public" }, { "url": "http://nova/novapi/internal", "region": "RegionOne", "interface": "internal" }, { "url": "http://nova/novapi/admin", "region": "RegionOne", "interface": "admin" }], "type": "compute", "name": "nova", }, { "endpoints": [{ "url": "http://glance/glanceapi/public", "region": "RegionOne", "interface": "public" }, { "url": "http://glance/glanceapi/internal", "region": "RegionOne", "interface": "internal" }, { "url": "http://glance/glanceapi/admin", "region": "RegionOne", "interface": "admin" }], "type": "image", "name": "glance" }, { "endpoints": [{ "url": "http://127.0.0.1:5000/v3", "region": "RegionOne", "interface": "public" }, { "url": "http://127.0.0.1:5000/v3", "region": "RegionOne", "interface": "internal" }, { "url": TEST_ADMIN_URL, "region": "RegionOne", "interface": "admin" }], "type": "identity" }, { "endpoints": [{ "url": "http://swift/swiftapi/public", "region": "RegionOne", "interface": "public" }, { "url": "http://swift/swiftapi/internal", "region": "RegionOne", "interface": "internal" }, { "url": "http://swift/swiftapi/admin", "region": "RegionOne", "interface": "admin" }], "type": "object-store" }] TEST_SERVICE_PROVIDERS = [ { "auth_url": "https://sp1.com/v3/OS-FEDERATION/" "identity_providers/acme/protocols/saml2/auth", "id": "sp1", "sp_url": "https://sp1.com/Shibboleth.sso/SAML2/ECP" }, { "auth_url": "https://sp2.com/v3/OS-FEDERATION/" "identity_providers/acme/protocols/saml2/auth", "id": "sp2", "sp_url": "https://sp2.com/Shibboleth.sso/SAML2/ECP" } ] def setUp(self): super(V3IdentityPlugin, self).setUp() self.TEST_DISCOVERY_RESPONSE = { 'versions': {'values': [fixture.V3Discovery(self.TEST_URL)]}} nextyear = 1 + time.gmtime().tm_year self.TEST_RESPONSE_DICT = { "token": { "methods": [ "token", "password" ], "expires_at": "%i-02-01T00:00:10.000123Z" % nextyear, "project": { "domain": { "id": self.TEST_DOMAIN_ID, "name": self.TEST_DOMAIN_NAME }, "id": self.TEST_TENANT_ID, "name": self.TEST_TENANT_NAME }, "user": { "domain": { "id": self.TEST_DOMAIN_ID, "name": self.TEST_DOMAIN_NAME }, "id": self.TEST_USER, "name": self.TEST_USER }, "issued_at": "2013-05-29T16:55:21.468960Z", "catalog": self.TEST_SERVICE_CATALOG, "service_providers": self.TEST_SERVICE_PROVIDERS }, } self.TEST_PROJECTS_RESPONSE = { "projects": [ { "domain_id": "1789d1", "enabled": "True", "id": "263fd9", "links": { "self": "https://identity:5000/v3/projects/263fd9" }, "name": "Dev Group A" }, { "domain_id": "1789d1", "enabled": "True", "id": "e56ad3", "links": { "self": "https://identity:5000/v3/projects/e56ad3" }, "name": "Dev Group B" } ], "links": { "self": "https://identity:5000/v3/projects", } } self.TEST_APP_CRED_TOKEN_RESPONSE = { "token": { "methods": [ "application_credential" ], "expires_at": "%i-02-01T00:00:10.000123Z" % nextyear, "project": { "domain": { "id": self.TEST_DOMAIN_ID, "name": self.TEST_DOMAIN_NAME }, "id": self.TEST_TENANT_ID, "name": self.TEST_TENANT_NAME }, "user": { "domain": { "id": self.TEST_DOMAIN_ID, "name": self.TEST_DOMAIN_NAME }, "id": self.TEST_USER, "name": self.TEST_USER }, "issued_at": "2013-05-29T16:55:21.468960Z", "catalog": self.TEST_SERVICE_CATALOG, "service_providers": self.TEST_SERVICE_PROVIDERS, "application_credential_restricted": True }, } self.TEST_RECEIPT_RESPONSE = { "receipt": { "methods": ["password"], "expires_at": "%i-02-01T00:00:10.000123Z" % nextyear, "user": { "domain": { "id": self.TEST_DOMAIN_ID, "name": self.TEST_DOMAIN_NAME, }, "id": self.TEST_USER, "name": self.TEST_USER, }, "issued_at": "2013-05-29T16:55:21.468960Z", }, "required_auth_methods": [["password", "totp"]], } def stub_auth(self, subject_token=None, **kwargs): if not subject_token: subject_token = self.TEST_TOKEN self.stub_url('POST', ['auth', 'tokens'], headers={'X-Subject-Token': subject_token}, **kwargs) def stub_receipt(self, receipt=None, receipt_data=None, **kwargs): if not receipt: receipt = self.TEST_RECEIPT if not receipt_data: receipt_data = self.TEST_RECEIPT_RESPONSE self.stub_url('POST', ['auth', 'tokens'], headers={'Openstack-Auth-Receipt': receipt}, status_code=401, json=receipt_data, **kwargs) def test_authenticate_with_username_password(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) self.assertFalse(a.has_scope_parameters) s = session.Session(auth=a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['password'], 'password': {'user': {'name': self.TEST_USER, 'password': self.TEST_PASS}}}}} self.assertRequestBodyIs(json=req) self.assertRequestHeaderEqual('Content-Type', 'application/json') self.assertRequestHeaderEqual('Accept', 'application/json') self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_authenticate_with_username_password_domain_scoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, domain_id=self.TEST_DOMAIN_ID) self.assertTrue(a.has_scope_parameters) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['password'], 'password': {'user': {'name': self.TEST_USER, 'password': self.TEST_PASS}}}, 'scope': {'domain': {'id': self.TEST_DOMAIN_ID}}}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_authenticate_with_username_password_project_scoped(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, project_id=self.TEST_TENANT_ID) self.assertTrue(a.has_scope_parameters) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['password'], 'password': {'user': {'name': self.TEST_USER, 'password': self.TEST_PASS}}}, 'scope': {'project': {'id': self.TEST_TENANT_ID}}}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) self.assertEqual(s.auth.auth_ref.project_id, self.TEST_TENANT_ID) def test_authenticate_with_token(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Token(self.TEST_URL, self.TEST_TOKEN) s = session.Session(auth=a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['token'], 'token': {'id': self.TEST_TOKEN}}}} self.assertRequestBodyIs(json=req) self.assertRequestHeaderEqual('Content-Type', 'application/json') self.assertRequestHeaderEqual('Accept', 'application/json') self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_with_expired(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) d = copy.deepcopy(self.TEST_RESPONSE_DICT) d['token']['expires_at'] = '2000-01-01T00:00:10.000123Z' a = v3.Password(self.TEST_URL, username='username', password='password') a.auth_ref = access.create(body=d) s = session.Session(auth=a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) self.assertEqual(a.auth_ref._data['token']['expires_at'], self.TEST_RESPONSE_DICT['token']['expires_at']) def test_with_domain_and_project_scoping(self): a = v3.Password(self.TEST_URL, username='username', password='password', project_id='project', domain_id='domain') self.assertTrue(a.has_scope_parameters) self.assertRaises(exceptions.AuthorizationFailure, a.get_token, None) self.assertRaises(exceptions.AuthorizationFailure, a.get_headers, None) def test_with_trust_id(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, trust_id='trust') self.assertTrue(a.has_scope_parameters) s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['password'], 'password': {'user': {'name': self.TEST_USER, 'password': self.TEST_PASS}}}, 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_with_multiple_mechanisms_factory(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) p = v3.PasswordMethod(username=self.TEST_USER, password=self.TEST_PASS) t = v3.TokenMethod(token='foo') a = v3.Auth(self.TEST_URL, [p, t], trust_id='trust') s = session.Session(a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['password', 'token'], 'password': {'user': {'name': self.TEST_USER, 'password': self.TEST_PASS}}, 'token': {'id': 'foo'}}, 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_with_multiple_mechanisms(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) p = v3.PasswordMethod(username=self.TEST_USER, password=self.TEST_PASS) t = v3.TokenMethod(token='foo') a = v3.Auth(self.TEST_URL, [p, t], trust_id='trust') self.assertTrue(a.has_scope_parameters) s = session.Session(auth=a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['password', 'token'], 'password': {'user': {'name': self.TEST_USER, 'password': self.TEST_PASS}}, 'token': {'id': 'foo'}}, 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}} self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_with_multiple_scopes(self): s = session.Session() a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, domain_id='x', project_id='x') self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, domain_id='x', trust_id='x') self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s) def test_application_credential_method(self): self.stub_auth(json=self.TEST_APP_CRED_TOKEN_RESPONSE) ac = v3.ApplicationCredential( self.TEST_URL, application_credential_id=self.TEST_APP_CRED_ID, application_credential_secret=self.TEST_APP_CRED_SECRET) req = {'auth': {'identity': {'methods': ['application_credential'], 'application_credential': { 'id': self.TEST_APP_CRED_ID, 'secret': self.TEST_APP_CRED_SECRET}}}} s = session.Session(auth=ac) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) self.assertRequestBodyIs(json=req) self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def _do_service_url_test(self, base_url, endpoint_filter): self.stub_auth(json=self.TEST_RESPONSE_DICT) self.stub_url('GET', ['path'], base_url=base_url, text='SUCCESS', status_code=200) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) resp = s.get('/path', endpoint_filter=endpoint_filter) self.assertEqual(resp.status_code, 200) self.assertEqual(self.requests_mock.last_request.url, base_url + '/path') def test_service_url(self): endpoint_filter = {'service_type': 'compute', 'interface': 'admin', 'service_name': 'nova'} self._do_service_url_test('http://nova/novapi/admin', endpoint_filter) def test_service_url_defaults_to_public(self): endpoint_filter = {'service_type': 'compute'} self._do_service_url_test('http://nova/novapi/public', endpoint_filter) def test_endpoint_filter_without_service_type_fails(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertRaises(exceptions.EndpointNotFound, s.get, '/path', endpoint_filter={'interface': 'admin'}) def test_full_url_overrides_endpoint_filter(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) self.stub_url('GET', [], base_url='http://testurl/', text='SUCCESS', status_code=200) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) resp = s.get('http://testurl/', endpoint_filter={'service_type': 'compute'}) self.assertEqual(resp.status_code, 200) self.assertEqual(resp.text, 'SUCCESS') def test_service_providers_urls(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session() auth_ref = a.get_auth_ref(s) service_providers = auth_ref.service_providers self.assertEqual('https://sp1.com/v3/OS-FEDERATION/' 'identity_providers/acme/protocols/saml2/auth', service_providers.get_auth_url('sp1')) self.assertEqual('https://sp1.com/Shibboleth.sso/SAML2/ECP', service_providers.get_sp_url('sp1')) self.assertEqual('https://sp2.com/v3/OS-FEDERATION/' 'identity_providers/acme/protocols/saml2/auth', service_providers.get_auth_url('sp2')) self.assertEqual('https://sp2.com/Shibboleth.sso/SAML2/ECP', service_providers.get_sp_url('sp2')) def test_handle_missing_service_provider(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session() auth_ref = a.get_auth_ref(s) service_providers = auth_ref.service_providers self.assertRaises(exceptions.ServiceProviderNotFound, service_providers._get_service_provider, uuid.uuid4().hex) def test_invalid_auth_response_dict(self): self.stub_auth(json={'hello': 'world'}) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', authenticated=True) def test_invalid_auth_response_type(self): self.stub_url('POST', ['auth', 'tokens'], text='testdata') a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any', authenticated=True) def test_invalidate_response(self): auth_responses = [{'status_code': 200, 'json': self.TEST_RESPONSE_DICT, 'headers': {'X-Subject-Token': 'token1'}}, {'status_code': 200, 'json': self.TEST_RESPONSE_DICT, 'headers': {'X-Subject-Token': 'token2'}}] self.requests_mock.post('%s/auth/tokens' % self.TEST_URL, auth_responses) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) s = session.Session(auth=a) self.assertEqual('token1', s.get_token()) self.assertEqual({'X-Auth-Token': 'token1'}, s.get_auth_headers()) a.invalidate() self.assertEqual('token2', s.get_token()) self.assertEqual({'X-Auth-Token': 'token2'}, s.get_auth_headers()) def test_doesnt_log_password(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) password = uuid.uuid4().hex a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=password) s = session.Session(a) self.assertEqual(self.TEST_TOKEN, s.get_token()) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) self.assertNotIn(password, self.logger.output) def test_sends_nocatalog(self): del self.TEST_RESPONSE_DICT['token']['catalog'] self.stub_auth(json=self.TEST_RESPONSE_DICT) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, include_catalog=False) s = session.Session(auth=a) s.get_token() auth_url = self.TEST_URL + '/auth/tokens' self.assertEqual(auth_url, a.token_url) self.assertEqual(auth_url + '?nocatalog', self.requests_mock.last_request.url) def test_symbols(self): self.assertIs(v3.AuthMethod, v3_base.AuthMethod) self.assertIs(v3.AuthConstructor, v3_base.AuthConstructor) self.assertIs(v3.Auth, v3_base.Auth) def test_unscoped_request(self): token = fixture.V3Token() self.stub_auth(json=token) password = uuid.uuid4().hex a = v3.Password(self.TEST_URL, user_id=token.user_id, password=password, unscoped=True) s = session.Session() auth_ref = a.get_access(s) self.assertFalse(auth_ref.scoped) body = self.requests_mock.last_request.json() ident = body['auth']['identity'] self.assertEqual(['password'], ident['methods']) self.assertEqual(token.user_id, ident['password']['user']['id']) self.assertEqual(password, ident['password']['user']['password']) self.assertEqual('unscoped', body['auth']['scope']) def test_unscoped_with_scope_data(self): a = v3.Password(self.TEST_URL, user_id=uuid.uuid4().hex, password=uuid.uuid4().hex, unscoped=True, project_id=uuid.uuid4().hex) s = session.Session() self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s) def test_password_cache_id(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) project_name = uuid.uuid4().hex a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, user_domain_id=self.TEST_DOMAIN_ID, project_domain_name=self.TEST_DOMAIN_NAME, project_name=project_name) b = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, user_domain_id=self.TEST_DOMAIN_ID, project_domain_name=self.TEST_DOMAIN_NAME, project_name=project_name) a_id = a.get_cache_id() b_id = b.get_cache_id() self.assertEqual(a_id, b_id) c = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, user_domain_id=self.TEST_DOMAIN_ID, project_domain_name=self.TEST_DOMAIN_NAME, project_id=project_name) # same value different param c_id = c.get_cache_id() self.assertNotEqual(a_id, c_id) self.assertIsNone(a.get_auth_state()) self.assertIsNone(b.get_auth_state()) self.assertIsNone(c.get_auth_state()) s = session.Session() self.assertEqual(self.TEST_TOKEN, a.get_token(s)) self.assertTrue(self.requests_mock.called) def test_password_change_auth_state(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) expired = ksa_utils.before_utcnow(days=2) token = fixture.V3Token(expires=expired) token_id = uuid.uuid4().hex state = json.dumps({'auth_token': token_id, 'body': token}) a = v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, user_domain_id=self.TEST_DOMAIN_ID, project_id=uuid.uuid4().hex) initial_cache_id = a.get_cache_id() self.assertIsNone(a.get_auth_state()) a.set_auth_state(state) self.assertEqual(token_id, a.auth_ref.auth_token) s = session.Session() self.assertEqual(self.TEST_TOKEN, a.get_token(s)) # updates expired self.assertEqual(initial_cache_id, a.get_cache_id()) def test_receipt_response_is_handled(self): self.stub_receipt() a = v3.Password( self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS, user_domain_id=self.TEST_DOMAIN_ID, project_id=self.TEST_TENANT_ID, ) s = session.Session(a) self.assertRaises(exceptions.MissingAuthMethods, s.get_auth_headers, None) def test_authenticate_with_receipt_and_totp(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) passcode = "123456" auth = v3.TOTP( self.TEST_URL, username=self.TEST_USER, passcode=passcode ) auth.add_method(v3.ReceiptMethod(receipt=self.TEST_RECEIPT)) self.assertFalse(auth.has_scope_parameters) s = session.Session(auth=auth) self.assertEqual({"X-Auth-Token": self.TEST_TOKEN}, s.get_auth_headers()) # NOTE(adriant): Here we are confirming the receipt data isn't in the # body or listed as a method req = { "auth": { "identity": { "methods": ["totp"], "totp": {"user": { "name": self.TEST_USER, "passcode": passcode}}, } } } self.assertRequestBodyIs(json=req) self.assertRequestHeaderEqual("Openstack-Auth-Receipt", self.TEST_RECEIPT) self.assertRequestHeaderEqual("Content-Type", "application/json") self.assertRequestHeaderEqual("Accept", "application/json") self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_authenticate_with_multi_factor(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) passcode = "123456" auth = v3.MultiFactor( self.TEST_URL, auth_methods=['v3password', 'v3totp'], username=self.TEST_USER, password=self.TEST_PASS, passcode=passcode, user_domain_id=self.TEST_DOMAIN_ID, project_id=self.TEST_TENANT_ID, ) self.assertTrue(auth.has_scope_parameters) s = session.Session(auth=auth) self.assertEqual({"X-Auth-Token": self.TEST_TOKEN}, s.get_auth_headers()) req = { "auth": { "identity": { "methods": ["password", "totp"], "totp": {"user": { "name": self.TEST_USER, "passcode": passcode, 'domain': {'id': self.TEST_DOMAIN_ID} }}, 'password': {'user': { 'name': self.TEST_USER, 'password': self.TEST_PASS, 'domain': {'id': self.TEST_DOMAIN_ID} }}, }, 'scope': {'project': {'id': self.TEST_TENANT_ID}} } } self.assertRequestBodyIs(json=req) self.assertRequestHeaderEqual("Content-Type", "application/json") self.assertRequestHeaderEqual("Accept", "application/json") self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) def test_authenticate_with_unversioned_endpoint(self): self.stub_auth(json=self.TEST_RESPONSE_DICT) # We use the root url here because it doesn't reference the API version # (e.g., '/v3'). We want to make sure the authentication plugin handles # this and appends /v3 if it's not present. a = v3.Password(self.TEST_ROOT_URL, username=self.TEST_USER, password=self.TEST_PASS) self.assertFalse(a.has_scope_parameters) s = session.Session(auth=a) self.assertEqual({'X-Auth-Token': self.TEST_TOKEN}, s.get_auth_headers()) req = {'auth': {'identity': {'methods': ['password'], 'password': {'user': {'name': self.TEST_USER, 'password': self.TEST_PASS}}}}} self.assertRequestBodyIs(json=req) self.assertRequestHeaderEqual('Content-Type', 'application/json') self.assertRequestHeaderEqual('Accept', 'application/json') self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/identity/test_identity_v3_federation.py0000664000175000017500000002577700000000000031220 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import uuid import six from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1 import identity from keystoneauth1.identity import v3 from keystoneauth1 import session from keystoneauth1.tests.unit import k2k_fixtures from keystoneauth1.tests.unit import utils class TesterFederationPlugin(v3.FederationBaseAuth): def get_unscoped_auth_ref(self, sess, **kwargs): # This would go and talk to an idp or something resp = sess.post(self.federated_token_url, authenticated=False) return access.create(resp=resp) class V3FederatedPlugin(utils.TestCase): AUTH_URL = 'http://keystone/v3' def setUp(self): super(V3FederatedPlugin, self).setUp() self.unscoped_token = fixture.V3Token() self.unscoped_token_id = uuid.uuid4().hex self.scoped_token = copy.deepcopy(self.unscoped_token) self.scoped_token.set_project_scope() self.scoped_token.methods.append('token') self.scoped_token_id = uuid.uuid4().hex s = self.scoped_token.add_service('compute', name='nova') s.add_standard_endpoints(public='http://nova/public', admin='http://nova/admin', internal='http://nova/internal') self.idp = uuid.uuid4().hex self.protocol = uuid.uuid4().hex self.token_url = ('%s/OS-FEDERATION/identity_providers/%s/protocols/%s' '/auth' % (self.AUTH_URL, self.idp, self.protocol)) headers = {'X-Subject-Token': self.unscoped_token_id} self.unscoped_mock = self.requests_mock.post(self.token_url, json=self.unscoped_token, headers=headers) headers = {'X-Subject-Token': self.scoped_token_id} auth_url = self.AUTH_URL + '/auth/tokens' self.scoped_mock = self.requests_mock.post(auth_url, json=self.scoped_token, headers=headers) def get_plugin(self, **kwargs): kwargs.setdefault('auth_url', self.AUTH_URL) kwargs.setdefault('protocol', self.protocol) kwargs.setdefault('identity_provider', self.idp) return TesterFederationPlugin(**kwargs) def test_federated_url(self): plugin = self.get_plugin() self.assertEqual(self.token_url, plugin.federated_token_url) def test_unscoped_behaviour(self): sess = session.Session(auth=self.get_plugin()) self.assertEqual(self.unscoped_token_id, sess.get_token()) self.assertTrue(self.unscoped_mock.called) self.assertFalse(self.scoped_mock.called) def test_scoped_behaviour(self): auth = self.get_plugin(project_id=self.scoped_token.project_id) sess = session.Session(auth=auth) self.assertEqual(self.scoped_token_id, sess.get_token()) self.assertTrue(self.unscoped_mock.called) self.assertTrue(self.scoped_mock.called) class K2KAuthPluginTest(utils.TestCase): TEST_ROOT_URL = 'http://127.0.0.1:5000/' TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') TEST_PASS = 'password' REQUEST_ECP_URL = TEST_URL + '/auth/OS-FEDERATION/saml2/ecp' SP_ROOT_URL = 'https://sp1.com/v3' SP_ID = 'sp1' SP_URL = 'https://sp1.com/Shibboleth.sso/SAML2/ECP' SP_AUTH_URL = (SP_ROOT_URL + '/OS-FEDERATION/identity_providers' '/testidp/protocols/saml2/auth') SERVICE_PROVIDER_DICT = { 'id': SP_ID, 'auth_url': SP_AUTH_URL, 'sp_url': SP_URL } def setUp(self): super(K2KAuthPluginTest, self).setUp() self.token_v3 = fixture.V3Token() self.token_v3.add_service_provider( self.SP_ID, self.SP_AUTH_URL, self.SP_URL) self.session = session.Session() self.k2kplugin = self.get_plugin() def _get_base_plugin(self): self.stub_url('POST', ['auth', 'tokens'], headers={'X-Subject-Token': uuid.uuid4().hex}, json=self.token_v3) return v3.Password(self.TEST_URL, username=self.TEST_USER, password=self.TEST_PASS) def _mock_k2k_flow_urls(self, redirect_code=302): # List versions available for auth self.requests_mock.get( self.TEST_URL, json={'version': fixture.V3Discovery(self.TEST_URL)}, headers={'Content-Type': 'application/json'}) # The IdP should return a ECP wrapped assertion when requested self.requests_mock.register_uri( 'POST', self.REQUEST_ECP_URL, content=six.b(k2k_fixtures.ECP_ENVELOPE), headers={'Content-Type': 'application/vnd.paos+xml'}, status_code=200) # The SP should respond with a redirect (302 or 303) self.requests_mock.register_uri( 'POST', self.SP_URL, content=six.b(k2k_fixtures.TOKEN_BASED_ECP), headers={'Content-Type': 'application/vnd.paos+xml'}, status_code=redirect_code) # Should not follow the redirect URL, but use the auth_url attribute self.requests_mock.register_uri( 'GET', self.SP_AUTH_URL, json=k2k_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': k2k_fixtures.UNSCOPED_TOKEN_HEADER}) def get_plugin(self, **kwargs): kwargs.setdefault('base_plugin', self._get_base_plugin()) kwargs.setdefault('service_provider', self.SP_ID) return v3.Keystone2Keystone(**kwargs) def test_remote_url(self): remote_auth_url = self.k2kplugin._remote_auth_url(self.SP_AUTH_URL) self.assertEqual(self.SP_ROOT_URL, remote_auth_url) def test_fail_getting_ecp_assertion(self): self.requests_mock.get( self.TEST_URL, json={'version': fixture.V3Discovery(self.TEST_URL)}, headers={'Content-Type': 'application/json'}) self.requests_mock.register_uri( 'POST', self.REQUEST_ECP_URL, status_code=401) self.assertRaises(exceptions.AuthorizationFailure, self.k2kplugin._get_ecp_assertion, self.session) def test_get_ecp_assertion_empty_response(self): self.requests_mock.get( self.TEST_URL, json={'version': fixture.V3Discovery(self.TEST_URL)}, headers={'Content-Type': 'application/json'}) self.requests_mock.register_uri( 'POST', self.REQUEST_ECP_URL, headers={'Content-Type': 'application/vnd.paos+xml'}, content=six.b(''), status_code=200) self.assertRaises(exceptions.InvalidResponse, self.k2kplugin._get_ecp_assertion, self.session) def test_get_ecp_assertion_wrong_headers(self): self.requests_mock.get( self.TEST_URL, json={'version': fixture.V3Discovery(self.TEST_URL)}, headers={'Content-Type': 'application/json'}) self.requests_mock.register_uri( 'POST', self.REQUEST_ECP_URL, headers={'Content-Type': uuid.uuid4().hex}, content=six.b(''), status_code=200) self.assertRaises(exceptions.InvalidResponse, self.k2kplugin._get_ecp_assertion, self.session) def test_send_ecp_authn_response(self): self._mock_k2k_flow_urls() # Perform the request response = self.k2kplugin._send_service_provider_ecp_authn_response( self.session, self.SP_URL, self.SP_AUTH_URL) # Check the response self.assertEqual(k2k_fixtures.UNSCOPED_TOKEN_HEADER, response.headers['X-Subject-Token']) def test_send_ecp_authn_response_303_redirect(self): self._mock_k2k_flow_urls(redirect_code=303) # Perform the request response = self.k2kplugin._send_service_provider_ecp_authn_response( self.session, self.SP_URL, self.SP_AUTH_URL) # Check the response self.assertEqual(k2k_fixtures.UNSCOPED_TOKEN_HEADER, response.headers['X-Subject-Token']) def test_end_to_end_workflow(self): self._mock_k2k_flow_urls() auth_ref = self.k2kplugin.get_auth_ref(self.session) self.assertEqual(k2k_fixtures.UNSCOPED_TOKEN_HEADER, auth_ref.auth_token) def test_end_to_end_workflow_303_redirect(self): self._mock_k2k_flow_urls(redirect_code=303) auth_ref = self.k2kplugin.get_auth_ref(self.session) self.assertEqual(k2k_fixtures.UNSCOPED_TOKEN_HEADER, auth_ref.auth_token) def test_end_to_end_with_generic_password(self): # List versions available for auth self.requests_mock.get( self.TEST_ROOT_URL, json=fixture.DiscoveryList(self.TEST_ROOT_URL), headers={'Content-Type': 'application/json'}) # The IdP should return a ECP wrapped assertion when requested self.requests_mock.register_uri( 'POST', self.REQUEST_ECP_URL, content=six.b(k2k_fixtures.ECP_ENVELOPE), headers={'Content-Type': 'application/vnd.paos+xml'}, status_code=200) # The SP should respond with a redirect (302 or 303) self.requests_mock.register_uri( 'POST', self.SP_URL, content=six.b(k2k_fixtures.TOKEN_BASED_ECP), headers={'Content-Type': 'application/vnd.paos+xml'}, status_code=302) # Should not follow the redirect URL, but use the auth_url attribute self.requests_mock.register_uri( 'GET', self.SP_AUTH_URL, json=k2k_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': k2k_fixtures.UNSCOPED_TOKEN_HEADER}) self.stub_url('POST', ['auth', 'tokens'], headers={'X-Subject-Token': uuid.uuid4().hex}, json=self.token_v3) plugin = identity.Password(self.TEST_ROOT_URL, username=self.TEST_USER, password=self.TEST_PASS, user_domain_id='default') k2kplugin = self.get_plugin(base_plugin=plugin) self.assertEqual(k2k_fixtures.UNSCOPED_TOKEN_HEADER, k2kplugin.get_token(self.session)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/identity/test_identity_v3_oidc.py0000664000175000017500000003733500000000000030007 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid import warnings from six.moves import urllib from keystoneauth1 import exceptions from keystoneauth1.identity.v3 import oidc from keystoneauth1 import session from keystoneauth1.tests.unit import oidc_fixtures from keystoneauth1.tests.unit import utils KEYSTONE_TOKEN_VALUE = uuid.uuid4().hex class BaseOIDCTests(object): def setUp(self): super(BaseOIDCTests, self).setUp() self.session = session.Session() self.AUTH_URL = 'http://keystone:5000/v3' self.IDENTITY_PROVIDER = 'bluepages' self.PROTOCOL = 'oidc' self.USER_NAME = 'oidc_user@example.com' self.PROJECT_NAME = 'foo project' self.PASSWORD = uuid.uuid4().hex self.CLIENT_ID = uuid.uuid4().hex self.CLIENT_SECRET = uuid.uuid4().hex self.ACCESS_TOKEN = uuid.uuid4().hex self.ACCESS_TOKEN_ENDPOINT = 'https://localhost:8020/oidc/token' self.FEDERATION_AUTH_URL = '%s/%s' % ( self.AUTH_URL, 'OS-FEDERATION/identity_providers/bluepages/protocols/oidc/auth') self.REDIRECT_URL = 'urn:ietf:wg:oauth:2.0:oob' self.CODE = '4/M9TNz2G9WVwYxSjx0w9AgA1bOmryJltQvOhQMq0czJs.cnLNVAfqwG' self.DISCOVERY_URL = ('https://localhost:8020/oidc/.well-known/' 'openid-configuration') self.GRANT_TYPE = None def test_grant_type_and_plugin_missmatch(self): self.assertRaises( exceptions.OidcGrantTypeMissmatch, self.plugin.__class__, self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, grant_type=uuid.uuid4().hex ) def test_can_pass_grant_type_but_warning_is_issued(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") self.plugin.__class__( self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, grant_type=self.GRANT_TYPE) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "grant_type" in str(w[-1].message) def test_discovery_not_found(self): self.requests_mock.get("http://not.found", status_code=404) plugin = self.plugin.__class__( self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, discovery_endpoint="http://not.found") self.assertRaises(exceptions.http.NotFound, plugin._get_discovery_document, self.session) def test_no_discovery(self): plugin = self.plugin.__class__( self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, access_token_endpoint=self.ACCESS_TOKEN_ENDPOINT, ) self.assertEqual(self.ACCESS_TOKEN_ENDPOINT, plugin.access_token_endpoint) def test_load_discovery(self): self.requests_mock.get(self.DISCOVERY_URL, json=oidc_fixtures.DISCOVERY_DOCUMENT) plugin = self.plugin.__class__(self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, discovery_endpoint=self.DISCOVERY_URL) self.assertEqual( oidc_fixtures.DISCOVERY_DOCUMENT["token_endpoint"], plugin._get_access_token_endpoint(self.session) ) def test_no_access_token_endpoint(self): plugin = self.plugin.__class__(self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET) self.assertRaises(exceptions.OidcAccessTokenEndpointNotFound, plugin._get_access_token_endpoint, self.session) def test_invalid_discovery_document(self): self.requests_mock.get(self.DISCOVERY_URL, json={}) plugin = self.plugin.__class__(self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, discovery_endpoint=self.DISCOVERY_URL) self.assertRaises(exceptions.InvalidOidcDiscoveryDocument, plugin._get_discovery_document, self.session) def test_load_discovery_override_by_endpoints(self): self.requests_mock.get(self.DISCOVERY_URL, json=oidc_fixtures.DISCOVERY_DOCUMENT) access_token_endpoint = uuid.uuid4().hex plugin = self.plugin.__class__( self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, discovery_endpoint=self.DISCOVERY_URL, access_token_endpoint=access_token_endpoint ) self.assertEqual(access_token_endpoint, plugin._get_access_token_endpoint(self.session)) def test_wrong_grant_type(self): self.requests_mock.get(self.DISCOVERY_URL, json={"grant_types_supported": ["foo", "bar"]}) plugin = self.plugin.__class__(self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, discovery_endpoint=self.DISCOVERY_URL) self.assertRaises(exceptions.OidcPluginNotSupported, plugin.get_unscoped_auth_ref, self.session) class OIDCClientCredentialsTests(BaseOIDCTests, utils.TestCase): def setUp(self): super(OIDCClientCredentialsTests, self).setUp() self.GRANT_TYPE = 'client_credentials' self.plugin = oidc.OidcClientCredentials( self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, access_token_endpoint=self.ACCESS_TOKEN_ENDPOINT, project_name=self.PROJECT_NAME) def test_initial_call_to_get_access_token(self): """Test initial call, expect JSON access token.""" # Mock the output that creates the access token self.requests_mock.post( self.ACCESS_TOKEN_ENDPOINT, json=oidc_fixtures.ACCESS_TOKEN_VIA_PASSWORD_RESP) # Prep all the values and send the request scope = 'profile email' payload = {'grant_type': self.GRANT_TYPE, 'scope': scope} self.plugin._get_access_token(self.session, payload) # Verify the request matches the expected structure last_req = self.requests_mock.last_request self.assertEqual(self.ACCESS_TOKEN_ENDPOINT, last_req.url) self.assertEqual('POST', last_req.method) encoded_payload = urllib.parse.urlencode(payload) self.assertEqual(encoded_payload, last_req.body) def test_second_call_to_protected_url(self): """Test subsequent call, expect Keystone token.""" # Mock the output that creates the keystone token self.requests_mock.post( self.FEDERATION_AUTH_URL, json=oidc_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': KEYSTONE_TOKEN_VALUE}) res = self.plugin._get_keystone_token(self.session, self.ACCESS_TOKEN) # Verify the request matches the expected structure self.assertEqual(self.FEDERATION_AUTH_URL, res.request.url) self.assertEqual('POST', res.request.method) headers = {'Authorization': 'Bearer ' + self.ACCESS_TOKEN} self.assertEqual(headers['Authorization'], res.request.headers['Authorization']) def test_end_to_end_workflow(self): """Test full OpenID Connect workflow.""" # Mock the output that creates the access token self.requests_mock.post( self.ACCESS_TOKEN_ENDPOINT, json=oidc_fixtures.ACCESS_TOKEN_VIA_PASSWORD_RESP) # Mock the output that creates the keystone token self.requests_mock.post( self.FEDERATION_AUTH_URL, json=oidc_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': KEYSTONE_TOKEN_VALUE}) response = self.plugin.get_unscoped_auth_ref(self.session) self.assertEqual(KEYSTONE_TOKEN_VALUE, response.auth_token) class OIDCPasswordTests(BaseOIDCTests, utils.TestCase): def setUp(self): super(OIDCPasswordTests, self).setUp() self.GRANT_TYPE = 'password' self.plugin = oidc.OidcPassword( self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, access_token_endpoint=self.ACCESS_TOKEN_ENDPOINT, project_name=self.PROJECT_NAME, username=self.USER_NAME, password=self.PASSWORD) def test_initial_call_to_get_access_token(self): """Test initial call, expect JSON access token.""" # Mock the output that creates the access token self.requests_mock.post( self.ACCESS_TOKEN_ENDPOINT, json=oidc_fixtures.ACCESS_TOKEN_VIA_PASSWORD_RESP) # Prep all the values and send the request grant_type = 'password' scope = 'profile email' payload = {'grant_type': grant_type, 'username': self.USER_NAME, 'password': self.PASSWORD, 'scope': scope} self.plugin._get_access_token(self.session, payload) # Verify the request matches the expected structure last_req = self.requests_mock.last_request self.assertEqual(self.ACCESS_TOKEN_ENDPOINT, last_req.url) self.assertEqual('POST', last_req.method) encoded_payload = urllib.parse.urlencode(payload) self.assertEqual(encoded_payload, last_req.body) def test_second_call_to_protected_url(self): """Test subsequent call, expect Keystone token.""" # Mock the output that creates the keystone token self.requests_mock.post( self.FEDERATION_AUTH_URL, json=oidc_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': KEYSTONE_TOKEN_VALUE}) res = self.plugin._get_keystone_token(self.session, self.ACCESS_TOKEN) # Verify the request matches the expected structure self.assertEqual(self.FEDERATION_AUTH_URL, res.request.url) self.assertEqual('POST', res.request.method) headers = {'Authorization': 'Bearer ' + self.ACCESS_TOKEN} self.assertEqual(headers['Authorization'], res.request.headers['Authorization']) def test_end_to_end_workflow(self): """Test full OpenID Connect workflow.""" # Mock the output that creates the access token self.requests_mock.post( self.ACCESS_TOKEN_ENDPOINT, json=oidc_fixtures.ACCESS_TOKEN_VIA_PASSWORD_RESP) # Mock the output that creates the keystone token self.requests_mock.post( self.FEDERATION_AUTH_URL, json=oidc_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': KEYSTONE_TOKEN_VALUE}) response = self.plugin.get_unscoped_auth_ref(self.session) self.assertEqual(KEYSTONE_TOKEN_VALUE, response.auth_token) class OIDCAuthorizationGrantTests(BaseOIDCTests, utils.TestCase): def setUp(self): super(OIDCAuthorizationGrantTests, self).setUp() self.GRANT_TYPE = 'authorization_code' self.plugin = oidc.OidcAuthorizationCode( self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, access_token_endpoint=self.ACCESS_TOKEN_ENDPOINT, redirect_uri=self.REDIRECT_URL, project_name=self.PROJECT_NAME, code=self.CODE) def test_initial_call_to_get_access_token(self): """Test initial call, expect JSON access token.""" # Mock the output that creates the access token self.requests_mock.post( self.ACCESS_TOKEN_ENDPOINT, json=oidc_fixtures.ACCESS_TOKEN_VIA_AUTH_GRANT_RESP) # Prep all the values and send the request grant_type = 'authorization_code' payload = {'grant_type': grant_type, 'redirect_uri': self.REDIRECT_URL, 'code': self.CODE} self.plugin._get_access_token(self.session, payload) # Verify the request matches the expected structure last_req = self.requests_mock.last_request self.assertEqual(self.ACCESS_TOKEN_ENDPOINT, last_req.url) self.assertEqual('POST', last_req.method) encoded_payload = urllib.parse.urlencode(payload) self.assertEqual(encoded_payload, last_req.body) # NOTE(aloga): This is a special case, as we do not need all the other openid # parameters, like client_id, client_secret, access_token_endpoint and so on, # therefore we do not inherit from the base oidc test class, but from the base # TestCase class OIDCTokenTests(utils.TestCase): def setUp(self): super(OIDCTokenTests, self).setUp() self.session = session.Session() self.AUTH_URL = 'http://keystone:5000/v3' self.IDENTITY_PROVIDER = 'bluepages' self.PROTOCOL = 'oidc' self.PROJECT_NAME = 'foo project' self.ACCESS_TOKEN = uuid.uuid4().hex self.FEDERATION_AUTH_URL = '%s/%s' % ( self.AUTH_URL, 'OS-FEDERATION/identity_providers/bluepages/protocols/oidc/auth') self.plugin = oidc.OidcAccessToken( self.AUTH_URL, self.IDENTITY_PROVIDER, self.PROTOCOL, access_token=self.ACCESS_TOKEN, project_name=self.PROJECT_NAME) def test_end_to_end_workflow(self): """Test full OpenID Connect workflow.""" # Mock the output that creates the keystone token self.requests_mock.post( self.FEDERATION_AUTH_URL, json=oidc_fixtures.UNSCOPED_TOKEN, headers={'X-Subject-Token': KEYSTONE_TOKEN_VALUE}) response = self.plugin.get_unscoped_auth_ref(self.session) self.assertEqual(KEYSTONE_TOKEN_VALUE, response.auth_token) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/identity/test_password.py0000664000175000017500000000760600000000000026410 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from keystoneauth1.identity.generic import password from keystoneauth1.identity import v2 from keystoneauth1.identity import v3 from keystoneauth1.identity.v3 import password as v3_password from keystoneauth1.tests.unit.identity import utils class PasswordTests(utils.GenericPluginTestCase): PLUGIN_CLASS = password.Password V2_PLUGIN_CLASS = v2.Password V3_PLUGIN_CLASS = v3.Password def new_plugin(self, **kwargs): kwargs.setdefault('username', uuid.uuid4().hex) kwargs.setdefault('password', uuid.uuid4().hex) return super(PasswordTests, self).new_plugin(**kwargs) def test_with_user_domain_params(self): self.stub_discovery() self.assertCreateV3(domain_id=uuid.uuid4().hex, user_domain_id=uuid.uuid4().hex) def test_v3_user_params_v2_url(self): self.stub_discovery(v3=False) self.assertDiscoveryFailure(user_domain_id=uuid.uuid4().hex) def test_v3_domain_params_v2_url(self): self.stub_discovery(v3=False) self.assertDiscoveryFailure(domain_id=uuid.uuid4().hex) def test_v3_disocovery_failure_v2_url(self): auth_url = self.TEST_URL + 'v2.0' self.stub_url('GET', json={}, base_url='/v2.0', status_code=500) self.assertDiscoveryFailure(domain_id=uuid.uuid4().hex, auth_url=auth_url) def test_symbols(self): self.assertIs(v3.Password, v3_password.Password) self.assertIs(v3.PasswordMethod, v3_password.PasswordMethod) def test_default_domain_id_with_v3(self): default_domain_id = uuid.uuid4().hex p = super(PasswordTests, self).test_default_domain_id_with_v3( default_domain_id=default_domain_id) self.assertEqual(default_domain_id, p._plugin.auth_methods[0].user_domain_id) def test_default_domain_name_with_v3(self): default_domain_name = uuid.uuid4().hex p = super(PasswordTests, self).test_default_domain_name_with_v3( default_domain_name=default_domain_name) self.assertEqual(default_domain_name, p._plugin.auth_methods[0].user_domain_name) def test_password_cache_id(self): username = uuid.uuid4().hex the_password = uuid.uuid4().hex project_name = uuid.uuid4().hex default_domain_id = uuid.uuid4().hex a = password.Password(self.TEST_URL, username=username, password=the_password, project_name=project_name, default_domain_id=default_domain_id) b = password.Password(self.TEST_URL, username=username, password=the_password, project_name=project_name, default_domain_id=default_domain_id) a_id = a.get_cache_id() b_id = b.get_cache_id() self.assertEqual(a_id, b_id) c = password.Password(self.TEST_URL, username=username, password=uuid.uuid4().hex, # different project_name=project_name, default_domain_id=default_domain_id) c_id = c.get_cache_id() self.assertNotEqual(a_id, c_id) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/identity/test_token.py0000664000175000017500000000415100000000000025656 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from keystoneauth1.identity.generic import token from keystoneauth1.identity import v2 from keystoneauth1.identity import v3 from keystoneauth1.identity.v3 import token as v3_token from keystoneauth1.tests.unit.identity import utils class TokenTests(utils.GenericPluginTestCase): PLUGIN_CLASS = token.Token V2_PLUGIN_CLASS = v2.Token V3_PLUGIN_CLASS = v3.Token def new_plugin(self, **kwargs): kwargs.setdefault('token', uuid.uuid4().hex) return super(TokenTests, self).new_plugin(**kwargs) def test_symbols(self): self.assertIs(v3.Token, v3_token.Token) self.assertIs(v3.TokenMethod, v3_token.TokenMethod) def test_token_cache_id(self): the_token = uuid.uuid4().hex project_name = uuid.uuid4().hex default_domain_id = uuid.uuid4().hex a = token.Token(self.TEST_URL, token=the_token, project_name=project_name, default_domain_id=default_domain_id) b = token.Token(self.TEST_URL, token=the_token, project_name=project_name, default_domain_id=default_domain_id) a_id = a.get_cache_id() b_id = b.get_cache_id() self.assertEqual(a_id, b_id) c = token.Token(self.TEST_URL, token=the_token, project_name=uuid.uuid4().hex, # different default_domain_id=default_domain_id) c_id = c.get_cache_id() self.assertNotEqual(a_id, c_id) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/identity/test_tokenless_auth.py0000664000175000017500000001100700000000000027564 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from keystoneauth1 import exceptions from keystoneauth1.identity.v3 import tokenless_auth from keystoneauth1 import session from keystoneauth1.tests.unit import utils class TokenlessAuthTest(utils.TestCase): TEST_URL = 'http://server/prefix' def create(self, auth_url, domain_id=None, domain_name=None, project_id=None, project_name=None, project_domain_id=None, project_domain_name=None): self.requests_mock.get(self.TEST_URL) auth = tokenless_auth.TokenlessAuth( auth_url=self.TEST_URL, domain_id=domain_id, domain_name=domain_name, project_id=project_id, project_name=project_name, project_domain_id=project_domain_id, project_domain_name=project_domain_name) return auth, session.Session(auth=auth) def test_domain_id_scope_header_pass(self): domain_id = uuid.uuid4().hex auth, session = self.create(auth_url=self.TEST_URL, domain_id=domain_id) session.get(self.TEST_URL, authenticated=True) self.assertRequestHeaderEqual('X-Domain-Id', domain_id) def test_domain_name_scope_header_pass(self): domain_name = uuid.uuid4().hex auth, session = self.create(auth_url=self.TEST_URL, domain_name=domain_name) session.get(self.TEST_URL, authenticated=True) self.assertRequestHeaderEqual('X-Domain-Name', domain_name) def test_project_id_scope_header_pass(self): project_id = uuid.uuid4().hex auth, session = self.create(auth_url=self.TEST_URL, project_id=project_id) session.get(self.TEST_URL, authenticated=True) self.assertRequestHeaderEqual('X-Project-Id', project_id) def test_project_of_domain_id_scope_header_pass(self): project_name = uuid.uuid4().hex project_domain_id = uuid.uuid4().hex auth, session = self.create(auth_url=self.TEST_URL, project_name=project_name, project_domain_id=project_domain_id) session.get(self.TEST_URL, authenticated=True) self.assertRequestHeaderEqual('X-Project-Name', project_name) self.assertRequestHeaderEqual('X-Project-Domain-Id', project_domain_id) def test_project_of_domain__name_scope_header_pass(self): project_name = uuid.uuid4().hex project_domain_name = uuid.uuid4().hex auth, session = self.create(auth_url=self.TEST_URL, project_name=project_name, project_domain_name=project_domain_name) session.get(self.TEST_URL, authenticated=True) self.assertRequestHeaderEqual('X-Project-Name', project_name) self.assertRequestHeaderEqual('X-Project-Domain-Name', project_domain_name) def test_no_scope_header_fail(self): auth, session = self.create(auth_url=self.TEST_URL) self.assertIsNone(auth.get_headers(session)) msg = 'No valid authentication is available' self.assertRaisesRegex(exceptions.AuthorizationFailure, msg, session.get, self.TEST_URL, authenticated=True) def test_project_name_scope_only_header_fail(self): project_name = uuid.uuid4().hex auth, session = self.create(auth_url=self.TEST_URL, project_name=project_name) self.assertIsNone(auth.get_headers(session)) msg = 'No valid authentication is available' self.assertRaisesRegex(exceptions.AuthorizationFailure, msg, session.get, self.TEST_URL, authenticated=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/identity/utils.py0000664000175000017500000001517400000000000024646 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from keystoneauth1 import access from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1 import session from keystoneauth1.tests.unit import utils class GenericPluginTestCase(utils.TestCase): TEST_URL = 'http://keystone.host:5000/' # OVERRIDE THESE IN SUB CLASSES PLUGIN_CLASS = None V2_PLUGIN_CLASS = None V3_PLUGIN_CLASS = None def setUp(self): super(GenericPluginTestCase, self).setUp() self.token_v2 = fixture.V2Token() self.token_v3 = fixture.V3Token() self.token_v3_id = uuid.uuid4().hex self.session = session.Session() self.stub_url('POST', ['v2.0', 'tokens'], json=self.token_v2) self.stub_url('POST', ['v3', 'auth', 'tokens'], headers={'X-Subject-Token': self.token_v3_id}, json=self.token_v3) def new_plugin(self, **kwargs): kwargs.setdefault('auth_url', self.TEST_URL) return self.PLUGIN_CLASS(**kwargs) def stub_discovery(self, base_url=None, **kwargs): kwargs.setdefault('href', self.TEST_URL) disc = fixture.DiscoveryList(**kwargs) self.stub_url('GET', json=disc, base_url=base_url, status_code=300) return disc def assertCreateV3(self, **kwargs): auth = self.new_plugin(**kwargs) auth_ref = auth.get_auth_ref(self.session) self.assertIsInstance(auth_ref, access.AccessInfoV3) self.assertEqual(self.TEST_URL + 'v3/auth/tokens', self.requests_mock.last_request.url) self.assertIsInstance(auth._plugin, self.V3_PLUGIN_CLASS) return auth def assertCreateV2(self, **kwargs): auth = self.new_plugin(**kwargs) auth_ref = auth.get_auth_ref(self.session) self.assertIsInstance(auth_ref, access.AccessInfoV2) self.assertEqual(self.TEST_URL + 'v2.0/tokens', self.requests_mock.last_request.url) self.assertIsInstance(auth._plugin, self.V2_PLUGIN_CLASS) return auth def assertDiscoveryFailure(self, **kwargs): plugin = self.new_plugin(**kwargs) self.assertRaises(exceptions.DiscoveryFailure, plugin.get_auth_ref, self.session) def test_create_v3_if_domain_params(self): self.stub_discovery() self.assertCreateV3(domain_id=uuid.uuid4().hex) self.assertCreateV3(domain_name=uuid.uuid4().hex) self.assertCreateV3(project_name=uuid.uuid4().hex, project_domain_name=uuid.uuid4().hex) self.assertCreateV3(project_name=uuid.uuid4().hex, project_domain_id=uuid.uuid4().hex) def test_create_v2_if_no_domain_params(self): self.stub_discovery() self.assertCreateV2() self.assertCreateV2(project_id=uuid.uuid4().hex) self.assertCreateV2(project_name=uuid.uuid4().hex) self.assertCreateV2(tenant_id=uuid.uuid4().hex) self.assertCreateV2(tenant_name=uuid.uuid4().hex) def test_create_plugin_no_reauthenticate(self): self.stub_discovery() self.assertCreateV2(reauthenticate=False) self.assertCreateV3(domain_id=uuid.uuid4().hex, reauthenticate=False) def test_v3_params_v2_url(self): self.stub_discovery(v3=False) self.assertDiscoveryFailure(domain_name=uuid.uuid4().hex) def test_v2_params_v3_url(self): self.stub_discovery(v2=False) self.assertCreateV3() def test_no_urls(self): self.stub_discovery(v2=False, v3=False) self.assertDiscoveryFailure() def test_path_based_url_v2(self): self.stub_url('GET', ['v2.0'], status_code=403) self.assertCreateV2(auth_url=self.TEST_URL + 'v2.0') def test_path_based_url_v3(self): self.stub_url('GET', ['v3'], status_code=403) self.assertCreateV3(auth_url=self.TEST_URL + 'v3') def test_disc_error_for_failure(self): self.stub_url('GET', [], status_code=403) self.assertDiscoveryFailure() self.assertIn(self.TEST_URL, self.logger.output) def test_v3_plugin_from_failure(self): url = self.TEST_URL + 'v3' self.stub_url('GET', [], base_url=url, status_code=403) self.assertCreateV3(auth_url=url) def test_unknown_discovery_version(self): # make a v4 entry that's mostly the same as a v3 self.stub_discovery(v2=False, v3_id='v4.0') self.assertDiscoveryFailure() def test_default_domain_id_with_v3(self, **kwargs): self.stub_discovery() project_name = uuid.uuid4().hex default_domain_id = kwargs.setdefault('default_domain_id', uuid.uuid4().hex) p = self.assertCreateV3(project_name=project_name, **kwargs) self.assertEqual(default_domain_id, p._plugin.project_domain_id) self.assertEqual(project_name, p._plugin.project_name) return p def test_default_domain_id_no_v3(self): self.stub_discovery(v3=False) project_name = uuid.uuid4().hex default_domain_id = uuid.uuid4().hex p = self.assertCreateV2(project_name=project_name, default_domain_id=default_domain_id) self.assertEqual(project_name, p._plugin.tenant_name) def test_default_domain_name_with_v3(self, **kwargs): self.stub_discovery() project_name = uuid.uuid4().hex default_domain_name = kwargs.setdefault('default_domain_name', uuid.uuid4().hex) p = self.assertCreateV3(project_name=project_name, **kwargs) self.assertEqual(default_domain_name, p._plugin.project_domain_name) self.assertEqual(project_name, p._plugin.project_name) return p def test_default_domain_name_no_v3(self): self.stub_discovery(v3=False) project_name = uuid.uuid4().hex default_domain_name = uuid.uuid4().hex p = self.assertCreateV2(project_name=project_name, default_domain_name=default_domain_name) self.assertEqual(project_name, p._plugin.tenant_name) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/k2k_fixtures.py0000664000175000017500000001251600000000000024272 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. UNSCOPED_TOKEN_HEADER = 'UNSCOPED_TOKEN' UNSCOPED_TOKEN = { "token": { "issued_at": "2014-06-09T09:48:59.643406Z", "extras": {}, "methods": ["token"], "expires_at": "2014-06-09T10:48:59.643375Z", "user": { "OS-FEDERATION": { "identity_provider": { "id": "testshib" }, "protocol": { "id": "saml2" }, "groups": [ {"id": "1764fa5cf69a49a4918131de5ce4af9a"} ] }, "id": "testhib%20user", "name": "testhib user" } } } SAML_ENCODING = "" TOKEN_SAML_RESPONSE = """ http://keystone.idp/v3/OS-FEDERATION/saml2/idp http://keystone.idp/v3/OS-FEDERATION/saml2/idp 0KH2CxdkfzU+6eiRhTC+mbObUKI= m2jh5gDvX/1k+4uKtbb08CHp2b9UWsLw ... admin urn:oasis:names:tc:SAML:2.0:ac:classes:Password http://keystone.idp/v3/OS-FEDERATION/saml2/idp admin admin admin """ TOKEN_BASED_SAML = ''.join([SAML_ENCODING, TOKEN_SAML_RESPONSE]) ECP_ENVELOPE = """ ss:mem:1ddfe8b0f58341a5a840d2e8717b0737 {0} """.format(TOKEN_SAML_RESPONSE) TOKEN_BASED_ECP = ''.join([SAML_ENCODING, ECP_ENVELOPE]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/keystoneauth_fixtures.py0000664000175000017500000000451200000000000026323 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import fixtures class HackingCode(fixtures.Fixture): """A fixture to house the various code examples. Examples contains various keystoneauth hacking style checks. """ oslo_namespace_imports = { 'code': """ import oslo.utils import oslo_utils import oslo.utils.encodeutils import oslo_utils.encodeutils from oslo import utils from oslo.utils import encodeutils from oslo_utils import encodeutils import oslo.serialization import oslo_serialization import oslo.serialization.jsonutils import oslo_serialization.jsonutils from oslo import serialization from oslo.serialization import jsonutils from oslo_serialization import jsonutils import oslo.config import oslo_config import oslo.config.cfg import oslo_config.cfg from oslo import config from oslo.config import cfg from oslo_config import cfg import oslo.i18n import oslo_i18n import oslo.i18n.log import oslo_i18n.log from oslo import i18n from oslo.i18n import log from oslo_i18n import log """, 'expected_errors': [ (1, 0, 'K333'), (3, 0, 'K333'), (5, 0, 'K333'), (6, 0, 'K333'), (9, 0, 'K333'), (11, 0, 'K333'), (13, 0, 'K333'), (14, 0, 'K333'), (17, 0, 'K333'), (19, 0, 'K333'), (21, 0, 'K333'), (22, 0, 'K333'), (25, 0, 'K333'), (27, 0, 'K333'), (29, 0, 'K333'), (30, 0, 'K333'), ], } ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3167944 keystoneauth1-4.4.0/keystoneauth1/tests/unit/loading/0000775000175000017500000000000000000000000022710 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/loading/__init__.py0000664000175000017500000000000000000000000025007 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/loading/test_adapter.py0000664000175000017500000002704700000000000025753 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from oslo_config import cfg from oslo_config import fixture as config from keystoneauth1 import loading from keystoneauth1.tests.unit.loading import utils class ConfLoadingTests(utils.TestCase): GROUP = 'adaptergroup' def setUp(self): super(ConfLoadingTests, self).setUp() self.conf_fx = self.useFixture(config.Config()) loading.register_adapter_conf_options(self.conf_fx.conf, self.GROUP, include_deprecated=False) def test_load(self): self.conf_fx.config( service_type='type', service_name='name', valid_interfaces='internal', region_name='region', endpoint_override='endpoint', version='2.0', group=self.GROUP) adap = loading.load_adapter_from_conf_options( self.conf_fx.conf, self.GROUP, session='session', auth='auth') self.assertEqual('type', adap.service_type) self.assertEqual('name', adap.service_name) self.assertEqual(['internal'], adap.interface) self.assertEqual('region', adap.region_name) self.assertEqual('endpoint', adap.endpoint_override) self.assertEqual('session', adap.session) self.assertEqual('auth', adap.auth) self.assertEqual('2.0', adap.version) self.assertIsNone(adap.min_version) self.assertIsNone(adap.max_version) def test_load_valid_interfaces_list(self): self.conf_fx.config( service_type='type', service_name='name', valid_interfaces=['internal', 'public'], region_name='region', endpoint_override='endpoint', version='2.0', group=self.GROUP) adap = loading.load_adapter_from_conf_options( self.conf_fx.conf, self.GROUP, session='session', auth='auth') self.assertEqual('type', adap.service_type) self.assertEqual('name', adap.service_name) self.assertEqual(['internal', 'public'], adap.interface) self.assertEqual('region', adap.region_name) self.assertEqual('endpoint', adap.endpoint_override) self.assertEqual('session', adap.session) self.assertEqual('auth', adap.auth) self.assertEqual('2.0', adap.version) self.assertIsNone(adap.min_version) self.assertIsNone(adap.max_version) def test_load_valid_interfaces_comma_list(self): self.conf_fx.config( service_type='type', service_name='name', valid_interfaces='internal,public', region_name='region', endpoint_override='endpoint', version='2.0', group=self.GROUP) adap = loading.load_adapter_from_conf_options( self.conf_fx.conf, self.GROUP, session='session', auth='auth') self.assertEqual('type', adap.service_type) self.assertEqual('name', adap.service_name) self.assertEqual(['internal', 'public'], adap.interface) self.assertEqual('region', adap.region_name) self.assertEqual('endpoint', adap.endpoint_override) self.assertEqual('session', adap.session) self.assertEqual('auth', adap.auth) self.assertEqual('2.0', adap.version) self.assertIsNone(adap.min_version) self.assertIsNone(adap.max_version) def test_load_bad_valid_interfaces_value(self): self.conf_fx.config( service_type='type', service_name='name', valid_interfaces='bad', region_name='region', endpoint_override='endpoint', version='2.0', group=self.GROUP) self.assertRaises( TypeError, loading.load_adapter_from_conf_options, self.conf_fx.conf, self.GROUP, session='session', auth='auth') def test_load_version_range(self): self.conf_fx.config( service_type='type', service_name='name', valid_interfaces='internal', region_name='region', endpoint_override='endpoint', min_version='2.0', max_version='3.0', group=self.GROUP) adap = loading.load_adapter_from_conf_options( self.conf_fx.conf, self.GROUP, session='session', auth='auth') self.assertEqual('type', adap.service_type) self.assertEqual('name', adap.service_name) self.assertEqual(['internal'], adap.interface) self.assertEqual('region', adap.region_name) self.assertEqual('endpoint', adap.endpoint_override) self.assertEqual('session', adap.session) self.assertEqual('auth', adap.auth) self.assertIsNone(adap.version) self.assertEqual('2.0', adap.min_version) self.assertEqual('3.0', adap.max_version) def test_version_mutex_min(self): self.conf_fx.config( service_type='type', service_name='name', valid_interfaces='iface', region_name='region', endpoint_override='endpoint', version='2.0', min_version='2.0', group=self.GROUP) self.assertRaises( TypeError, loading.load_adapter_from_conf_options, self.conf_fx.conf, self.GROUP, session='session', auth='auth') def test_version_mutex_max(self): self.conf_fx.config( service_type='type', service_name='name', valid_interfaces='iface', region_name='region', endpoint_override='endpoint', version='2.0', max_version='3.0', group=self.GROUP) self.assertRaises( TypeError, loading.load_adapter_from_conf_options, self.conf_fx.conf, self.GROUP, session='session', auth='auth') def test_version_mutex_minmax(self): self.conf_fx.config( service_type='type', service_name='name', valid_interfaces='iface', region_name='region', endpoint_override='endpoint', version='2.0', min_version='2.0', max_version='3.0', group=self.GROUP) self.assertRaises( TypeError, loading.load_adapter_from_conf_options, self.conf_fx.conf, self.GROUP, session='session', auth='auth') def test_load_retries(self): self.conf_fx.config( service_type='type', service_name='name', connect_retries=3, status_code_retries=5, connect_retry_delay=0.5, status_code_retry_delay=2.0, group=self.GROUP) adap = loading.load_adapter_from_conf_options( self.conf_fx.conf, self.GROUP, session='session', auth='auth') self.assertEqual('type', adap.service_type) self.assertEqual('name', adap.service_name) self.assertEqual(3, adap.connect_retries) self.assertEqual(0.5, adap.connect_retry_delay) self.assertEqual(5, adap.status_code_retries) self.assertEqual(2.0, adap.status_code_retry_delay) def test_get_conf_options(self): opts = loading.get_adapter_conf_options() for opt in opts: if opt.name.endswith('-retries'): self.assertIsInstance(opt, cfg.IntOpt) elif opt.name.endswith('-retry-delay'): self.assertIsInstance(opt, cfg.FloatOpt) elif opt.name != 'valid-interfaces': self.assertIsInstance(opt, cfg.StrOpt) else: self.assertIsInstance(opt, cfg.ListOpt) self.assertEqual({'service-type', 'service-name', 'interface', 'valid-interfaces', 'region-name', 'endpoint-override', 'version', 'min-version', 'max-version', 'connect-retries', 'status-code-retries', 'connect-retry-delay', 'status-code-retry-delay'}, {opt.name for opt in opts}) def test_get_conf_options_undeprecated(self): opts = loading.get_adapter_conf_options(include_deprecated=False) for opt in opts: if opt.name.endswith('-retries'): self.assertIsInstance(opt, cfg.IntOpt) elif opt.name.endswith('-retry-delay'): self.assertIsInstance(opt, cfg.FloatOpt) elif opt.name != 'valid-interfaces': self.assertIsInstance(opt, cfg.StrOpt) else: self.assertIsInstance(opt, cfg.ListOpt) self.assertEqual({'service-type', 'service-name', 'valid-interfaces', 'region-name', 'endpoint-override', 'version', 'min-version', 'max-version', 'connect-retries', 'status-code-retries', 'connect-retry-delay', 'status-code-retry-delay'}, {opt.name for opt in opts}) def test_deprecated(self): """Test external options that are deprecated by Adapter options. Not to be confused with ConfLoadingDeprecatedTests, which tests conf options in Adapter which are themselves deprecated. """ def new_deprecated(): return cfg.DeprecatedOpt(uuid.uuid4().hex, group=uuid.uuid4().hex) opt_names = ['service-type', 'valid-interfaces', 'endpoint-override'] depr = dict([(n, [new_deprecated()]) for n in opt_names]) opts = loading.get_adapter_conf_options(deprecated_opts=depr) for opt in opts: if opt.name in opt_names: self.assertIn(depr[opt.name][0], opt.deprecated_opts) class ConfLoadingLegacyTests(ConfLoadingTests): """Tests with inclusion of deprecated conf options. Not to be confused with ConfLoadingTests.test_deprecated, which tests external options that are deprecated in favor of Adapter options. """ GROUP = 'adaptergroup' def setUp(self): super(ConfLoadingLegacyTests, self).setUp() self.conf_fx = self.useFixture(config.Config()) loading.register_adapter_conf_options(self.conf_fx.conf, self.GROUP) def test_load_old_interface(self): self.conf_fx.config( service_type='type', service_name='name', interface='internal', region_name='region', endpoint_override='endpoint', version='2.0', group=self.GROUP) adap = loading.load_adapter_from_conf_options( self.conf_fx.conf, self.GROUP, session='session', auth='auth') self.assertEqual('type', adap.service_type) self.assertEqual('name', adap.service_name) self.assertEqual('internal', adap.interface) self.assertEqual('region', adap.region_name) self.assertEqual('endpoint', adap.endpoint_override) self.assertEqual('session', adap.session) self.assertEqual('auth', adap.auth) self.assertEqual('2.0', adap.version) self.assertIsNone(adap.min_version) self.assertIsNone(adap.max_version) def test_interface_conflict(self): self.conf_fx.config( service_type='type', service_name='name', interface='iface', valid_interfaces='internal,public', region_name='region', endpoint_override='endpoint', group=self.GROUP) self.assertRaises( TypeError, loading.load_adapter_from_conf_options, self.conf_fx.conf, self.GROUP, session='session', auth='auth') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/loading/test_cli.py0000664000175000017500000001767300000000000025106 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse from unittest import mock import uuid import fixtures from keystoneauth1 import loading from keystoneauth1.loading import cli from keystoneauth1.tests.unit.loading import utils TesterPlugin, TesterLoader = utils.create_plugin( opts=[ loading.Opt('test-opt', help='tester', deprecated=[loading.Opt('test-other')]) ] ) class CliTests(utils.TestCase): def setUp(self): super(CliTests, self).setUp() self.p = argparse.ArgumentParser() def env(self, name, value=None): if value is not None: # environment variables are always strings value = str(value) return self.useFixture(fixtures.EnvironmentVariable(name, value)) def test_creating_with_no_args(self): ret = loading.register_auth_argparse_arguments(self.p, []) self.assertIsNone(ret) self.assertIn('--os-auth-type', self.p.format_usage()) def test_load_with_nothing(self): loading.register_auth_argparse_arguments(self.p, []) opts = self.p.parse_args([]) self.assertIsNone(loading.load_auth_from_argparse_arguments(opts)) @utils.mock_plugin() def test_basic_params_added(self, m): name = uuid.uuid4().hex argv = ['--os-auth-plugin', name] ret = loading.register_auth_argparse_arguments(self.p, argv) self.assertIsInstance(ret, utils.MockLoader) for n in ('--os-a-int', '--os-a-bool', '--os-a-float'): self.assertIn(n, self.p.format_usage()) m.assert_called_once_with(name) @utils.mock_plugin() def test_param_loading(self, m): name = uuid.uuid4().hex argv = ['--os-auth-type', name, '--os-a-int', str(self.a_int), '--os-a-float', str(self.a_float), '--os-a-bool', str(self.a_bool)] klass = loading.register_auth_argparse_arguments(self.p, argv) self.assertIsInstance(klass, utils.MockLoader) opts = self.p.parse_args(argv) self.assertEqual(name, opts.os_auth_type) a = loading.load_auth_from_argparse_arguments(opts) self.assertTestVals(a) self.assertEqual(name, opts.os_auth_type) self.assertEqual(str(self.a_int), opts.os_a_int) self.assertEqual(str(self.a_float), opts.os_a_float) self.assertEqual(str(self.a_bool), opts.os_a_bool) @utils.mock_plugin() def test_default_options(self, m): name = uuid.uuid4().hex argv = ['--os-auth-type', name, '--os-a-float', str(self.a_float)] klass = loading.register_auth_argparse_arguments(self.p, argv) self.assertIsInstance(klass, utils.MockLoader) opts = self.p.parse_args(argv) self.assertEqual(name, opts.os_auth_type) a = loading.load_auth_from_argparse_arguments(opts) self.assertEqual(self.a_float, a['a_float']) self.assertEqual(3, a['a_int']) @utils.mock_plugin() def test_with_default_string_value(self, m): name = uuid.uuid4().hex klass = loading.register_auth_argparse_arguments(self.p, [], default=name) self.assertIsInstance(klass, utils.MockLoader) m.assert_called_once_with(name) @utils.mock_plugin() def test_overrides_default_string_value(self, m): name = uuid.uuid4().hex default = uuid.uuid4().hex argv = ['--os-auth-type', name] klass = loading.register_auth_argparse_arguments(self.p, argv, default=default) self.assertIsInstance(klass, utils.MockLoader) m.assert_called_once_with(name) @utils.mock_plugin() def test_with_default_type_value(self, m): default = utils.MockLoader() klass = loading.register_auth_argparse_arguments(self.p, [], default=default) self.assertIsInstance(klass, utils.MockLoader) self.assertEqual(0, m.call_count) @utils.mock_plugin() def test_overrides_default_type_value(self, m): # using this test plugin would fail if called because there # is no get_options() function class TestLoader(object): pass name = uuid.uuid4().hex argv = ['--os-auth-type', name] klass = loading.register_auth_argparse_arguments(self.p, argv, default=TestLoader) self.assertIsInstance(klass, utils.MockLoader) m.assert_called_once_with(name) @utils.mock_plugin() def test_env_overrides_default_opt(self, m): name = uuid.uuid4().hex val = uuid.uuid4().hex self.env('OS_A_STR', val) klass = loading.register_auth_argparse_arguments(self.p, [], default=name) self.assertIsInstance(klass, utils.MockLoader) opts = self.p.parse_args([]) a = loading.load_auth_from_argparse_arguments(opts) self.assertEqual(val, a['a_str']) def test_deprecated_cli_options(self): cli._register_plugin_argparse_arguments(self.p, TesterLoader()) val = uuid.uuid4().hex opts = self.p.parse_args(['--os-test-other', val]) self.assertEqual(val, opts.os_test_opt) def test_deprecated_multi_cli_options(self): cli._register_plugin_argparse_arguments(self.p, TesterLoader()) val1 = uuid.uuid4().hex val2 = uuid.uuid4().hex # argarse rules say that the last specified wins. opts = self.p.parse_args(['--os-test-other', val2, '--os-test-opt', val1]) self.assertEqual(val1, opts.os_test_opt) def test_deprecated_env_options(self): val = uuid.uuid4().hex with mock.patch.dict('os.environ', {'OS_TEST_OTHER': val}): cli._register_plugin_argparse_arguments(self.p, TesterLoader()) opts = self.p.parse_args([]) self.assertEqual(val, opts.os_test_opt) def test_deprecated_env_multi_options(self): val1 = uuid.uuid4().hex val2 = uuid.uuid4().hex with mock.patch.dict('os.environ', {'OS_TEST_OPT': val1, 'OS_TEST_OTHER': val2}): cli._register_plugin_argparse_arguments(self.p, TesterLoader()) opts = self.p.parse_args([]) self.assertEqual(val1, opts.os_test_opt) def test_adapter_service_type(self): argv = ['--os-service-type', 'compute'] loading.register_adapter_argparse_arguments(self.p, 'compute') opts = self.p.parse_args(argv) self.assertEqual('compute', opts.os_service_type) self.assertFalse(hasattr(opts, 'os_compute_service_type')) def test_adapter_service_type_per_service(self): argv = ['--os-compute-service-type', 'weirdness'] loading.register_adapter_argparse_arguments(self.p, 'compute') loading.register_service_adapter_argparse_arguments(self.p, 'compute') opts = self.p.parse_args(argv) self.assertEqual('compute', opts.os_service_type) self.assertEqual('weirdness', opts.os_compute_service_type) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/loading/test_conf.py0000664000175000017500000002027500000000000025254 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import uuid from oslo_config import cfg from oslo_config import fixture as config import stevedore from keystoneauth1 import exceptions from keystoneauth1 import loading from keystoneauth1.loading._plugins.identity import v2 from keystoneauth1.loading._plugins.identity import v3 from keystoneauth1.tests.unit.loading import utils class ConfTests(utils.TestCase): def setUp(self): super(ConfTests, self).setUp() self.conf_fixture = self.useFixture(config.Config()) # NOTE(jamielennox): we register the basic config options first because # we need them in place before we can stub them. We will need to run # the register again after we stub the auth section and auth plugin so # it can load the plugin specific options. loading.register_auth_conf_options(self.conf_fixture.conf, group=self.GROUP) def test_loading_v2(self): section = uuid.uuid4().hex auth_url = uuid.uuid4().hex username = uuid.uuid4().hex password = uuid.uuid4().hex trust_id = uuid.uuid4().hex tenant_id = uuid.uuid4().hex self.conf_fixture.config(auth_section=section, group=self.GROUP) loading.register_auth_conf_options(self.conf_fixture.conf, group=self.GROUP) opts = loading.get_auth_plugin_conf_options(v2.Password()) self.conf_fixture.register_opts(opts, group=section) self.conf_fixture.config(auth_type=self.V2PASS, auth_url=auth_url, username=username, password=password, trust_id=trust_id, tenant_id=tenant_id, group=section) a = loading.load_auth_from_conf_options(self.conf_fixture.conf, self.GROUP) self.assertEqual(auth_url, a.auth_url) self.assertEqual(username, a.username) self.assertEqual(password, a.password) self.assertEqual(trust_id, a.trust_id) self.assertEqual(tenant_id, a.tenant_id) def test_loading_v3(self): section = uuid.uuid4().hex auth_url = uuid.uuid4().hex, token = uuid.uuid4().hex trust_id = uuid.uuid4().hex project_id = uuid.uuid4().hex project_domain_name = uuid.uuid4().hex self.conf_fixture.config(auth_section=section, group=self.GROUP) loading.register_auth_conf_options(self.conf_fixture.conf, group=self.GROUP) opts = loading.get_auth_plugin_conf_options(v3.Token()) self.conf_fixture.register_opts(opts, group=section) self.conf_fixture.config(auth_type=self.V3TOKEN, auth_url=auth_url, token=token, trust_id=trust_id, project_id=project_id, project_domain_name=project_domain_name, group=section) a = loading.load_auth_from_conf_options(self.conf_fixture.conf, self.GROUP) self.assertEqual(token, a.auth_methods[0].token) self.assertEqual(trust_id, a.trust_id) self.assertEqual(project_id, a.project_id) self.assertEqual(project_domain_name, a.project_domain_name) def test_loading_invalid_plugin(self): auth_type = uuid.uuid4().hex self.conf_fixture.config(auth_type=auth_type, group=self.GROUP) e = self.assertRaises(exceptions.NoMatchingPlugin, loading.load_auth_from_conf_options, self.conf_fixture.conf, self.GROUP) self.assertEqual(auth_type, e.name) def test_loading_with_no_data(self): lo = loading.load_auth_from_conf_options(self.conf_fixture.conf, self.GROUP) self.assertIsNone(lo) @mock.patch('stevedore.DriverManager') def test_other_params(self, m): m.return_value = utils.MockManager(utils.MockLoader()) driver_name = uuid.uuid4().hex opts = loading.get_auth_plugin_conf_options(utils.MockLoader()) self.conf_fixture.register_opts(opts, group=self.GROUP) self.conf_fixture.config(auth_type=driver_name, group=self.GROUP, **self.TEST_VALS) a = loading.load_auth_from_conf_options(self.conf_fixture.conf, self.GROUP) self.assertTestVals(a) m.assert_called_once_with(namespace=loading.PLUGIN_NAMESPACE, name=driver_name, invoke_on_load=True) @utils.mock_plugin() def test_same_section(self, m): opts = loading.get_auth_plugin_conf_options(utils.MockLoader()) self.conf_fixture.register_opts(opts, group=self.GROUP) loading.register_auth_conf_options(self.conf_fixture.conf, group=self.GROUP) self.conf_fixture.config(auth_type=uuid.uuid4().hex, group=self.GROUP, **self.TEST_VALS) a = loading.load_auth_from_conf_options(self.conf_fixture.conf, self.GROUP) self.assertTestVals(a) @utils.mock_plugin() def test_diff_section(self, m): section = uuid.uuid4().hex self.conf_fixture.config(auth_section=section, group=self.GROUP) loading.register_auth_conf_options(self.conf_fixture.conf, group=self.GROUP) opts = loading.get_auth_plugin_conf_options(utils.MockLoader()) self.conf_fixture.register_opts(opts, group=section) self.conf_fixture.config(group=section, auth_type=uuid.uuid4().hex, **self.TEST_VALS) a = loading.load_auth_from_conf_options(self.conf_fixture.conf, self.GROUP) self.assertTestVals(a) def test_plugins_are_all_opts(self): manager = stevedore.ExtensionManager(loading.PLUGIN_NAMESPACE, propagate_map_exceptions=True) def inner(driver): for p in driver.plugin().get_options(): self.assertIsInstance(p, loading.Opt) manager.map(inner) def test_get_common(self): opts = loading.get_auth_common_conf_options() for opt in opts: self.assertIsInstance(opt, cfg.Opt) self.assertEqual(2, len(opts)) def test_get_named(self): loaded_opts = loading.get_plugin_options('v2password') plugin_opts = v2.Password().get_options() loaded_names = set([o.name for o in loaded_opts]) plugin_names = set([o.name for o in plugin_opts]) self.assertEqual(plugin_names, loaded_names) def test_register_cfg(self): loading.register_auth_conf_options(self.conf_fixture.conf, group=self.GROUP) def test_common_conf_options(self): opts = loading.get_auth_common_conf_options() self.assertEqual(2, len(opts)) auth_type = [o for o in opts if o.name == 'auth_type'][0] self.assertEqual(1, len(auth_type.deprecated_opts)) self.assertIsInstance(auth_type.deprecated_opts[0], cfg.DeprecatedOpt) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/loading/test_entry_points.py0000664000175000017500000000236200000000000027061 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import stevedore from keystoneauth1 import loading from keystoneauth1.tests.unit.loading import utils class EntryPointTests(utils.TestCase): """Simple test that will check that all entry points are loadable.""" def test_all_entry_points_are_valid(self): errors = [] def raise_exception_callback(manager, entrypoint, exc): error = ("Cannot load '%(entrypoint)s' entry_point: %(error)s'" % {"entrypoint": entrypoint, "error": exc}) errors.append(error) stevedore.ExtensionManager( namespace=loading.PLUGIN_NAMESPACE, on_load_failure_callback=raise_exception_callback ) self.assertEqual([], errors) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/loading/test_fixtures.py0000664000175000017500000000655600000000000026206 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from oslo_config import fixture as config from keystoneauth1 import fixture from keystoneauth1 import loading from keystoneauth1 import session from keystoneauth1.tests.unit import utils class FixturesTests(utils.TestCase): GROUP = uuid.uuid4().hex AUTH_TYPE = uuid.uuid4().hex def setUp(self): super(FixturesTests, self).setUp() self.conf_fixture = self.useFixture(config.Config()) # conf loading will still try to read the auth_type from the config # object and pass that to the get_plugin_loader method. This value will # typically be ignored and the fake plugin returned regardless of name # but it could be a useful differentiator and it also ensures that the # application has called register_auth_conf_options before simply # returning a fake plugin. loading.register_auth_conf_options(self.conf_fixture.conf, group=self.GROUP) self.conf_fixture.config(auth_type=self.AUTH_TYPE, group=self.GROUP) def useLoadingFixture(self, **kwargs): return self.useFixture(fixture.LoadingFixture(**kwargs)) def test_endpoint_resolve(self): endpoint = "http://%(service_type)s/%(version)s/%(interface)s" loader = self.useLoadingFixture(endpoint=endpoint) endpoint_filter = {'service_type': 'compute', 'service_name': 'nova', 'version': (2, 1), 'interface': 'public'} auth = loading.load_auth_from_conf_options(self.conf_fixture.conf, self.GROUP) sess = session.Session(auth=auth) loader_endpoint = loader.get_endpoint(**endpoint_filter) plugin_endpoint = sess.get_endpoint(**endpoint_filter) self.assertEqual("http://compute/2.1/public", loader_endpoint) self.assertEqual(loader_endpoint, plugin_endpoint) def test_conf_loaded(self): token = uuid.uuid4().hex endpoint_filter = {'service_type': 'compute', 'service_name': 'nova', 'version': (2, 1)} loader = self.useLoadingFixture(token=token) url = loader.get_endpoint('/path', **endpoint_filter) m = self.requests_mock.get(url) auth = loading.load_auth_from_conf_options(self.conf_fixture.conf, self.GROUP) sess = session.Session(auth=auth) self.assertEqual(self.AUTH_TYPE, auth.auth_type) sess.get('/path', endpoint_filter=endpoint_filter) self.assertTrue(m.called_once) self.assertTrue(token, m.last_request.headers['X-Auth-Token']) self.assertEqual(loader.project_id, sess.get_project_id()) self.assertEqual(loader.user_id, sess.get_user_id()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/loading/test_generic.py0000664000175000017500000000604200000000000025737 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from keystoneauth1 import fixture from keystoneauth1 import identity from keystoneauth1.loading._plugins.identity import generic from keystoneauth1 import session from keystoneauth1.tests.unit.loading import utils class PasswordTests(utils.TestCase): def test_options(self): opts = [o.name for o in generic.Password().get_options()] allowed_opts = ['username', 'user-domain-id', 'user-domain-name', 'user-id', 'password', 'system-scope', 'domain-id', 'domain-name', 'project-id', 'project-name', 'project-domain-id', 'project-domain-name', 'trust-id', 'auth-url', 'default-domain-id', 'default-domain-name', ] self.assertEqual(set(allowed_opts), set(opts)) self.assertEqual(len(allowed_opts), len(opts)) def test_loads_v3_with_user_domain(self): auth_url = 'http://keystone.test:5000' disc = fixture.DiscoveryList(href=auth_url) sess = session.Session() self.requests_mock.get(auth_url, json=disc) plugin = generic.Password().load_from_options( auth_url=auth_url, user_id=uuid.uuid4().hex, password=uuid.uuid4().hex, project_id=uuid.uuid4().hex, user_domain_id=uuid.uuid4().hex) inner_plugin = plugin._do_create_plugin(sess) self.assertIsInstance(inner_plugin, identity.V3Password) self.assertEqual(inner_plugin.auth_url, auth_url + '/v3') class TokenTests(utils.TestCase): def test_options(self): opts = [o.name for o in generic.Token().get_options()] allowed_opts = ['token', 'system-scope', 'domain-id', 'domain-name', 'project-id', 'project-name', 'project-domain-id', 'project-domain-name', 'trust-id', 'auth-url', 'default-domain-id', 'default-domain-name', ] self.assertEqual(set(allowed_opts), set(opts)) self.assertEqual(len(allowed_opts), len(opts)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/loading/test_loading.py0000664000175000017500000001026700000000000025744 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from testtools import matchers from keystoneauth1 import exceptions from keystoneauth1 import loading from keystoneauth1.tests.unit.loading import utils class PluginA(object): def __init__(self, a): self.val = a class PluginB(object): def __init__(self, b): self.val = b class TestSplitLoader(loading.BaseLoader): def get_options(self): opts = super(TestSplitLoader, self).get_options() opts += [loading.Opt('a'), loading.Opt('b')] return opts def create_plugin(self, a=None, b=None, **kwargs): if a: return PluginA(a) if b: return PluginB(b) raise AssertionError('Expected A or B') class LoadingTests(utils.TestCase): def test_required_values(self): opts = [loading.Opt('a', required=False), loading.Opt('b', required=True)] Plugin, Loader = utils.create_plugin(opts=opts) lo = Loader() v = uuid.uuid4().hex p1 = lo.load_from_options(b=v) self.assertEqual(v, p1['b']) e = self.assertRaises(exceptions.MissingRequiredOptions, lo.load_from_options, a=v) self.assertEqual(1, len(e.options)) for o in e.options: self.assertIsInstance(o, loading.Opt) self.assertEqual('b', e.options[0].name) def test_loaders(self): loaders = loading.get_available_plugin_loaders() self.assertThat(len(loaders), matchers.GreaterThan(0)) for loader in loaders.values(): self.assertIsInstance(loader, loading.BaseLoader) def test_loading_getter(self): called_opts = [] vals = {'a-int': 44, 'a-bool': False, 'a-float': 99.99, 'a-str': 'value'} val = uuid.uuid4().hex def _getter(opt): called_opts.append(opt.name) # return str because oslo.config should convert them back return str(vals[opt.name]) p = utils.MockLoader().load_from_options_getter(_getter, other=val) self.assertEqual(set(vals), set(called_opts)) for k, v in vals.items(): # replace - to _ because it's the dest used to create kwargs self.assertEqual(v, p[k.replace('-', '_')]) # check that additional kwargs get passed through self.assertEqual(val, p['other']) def test_loading_getter_with_kwargs(self): called_opts = set() vals = {'a-bool': False, 'a-float': 99.99} def _getter(opt): called_opts.add(opt.name) # return str because oslo.config should convert them back return str(vals[opt.name]) p = utils.MockLoader().load_from_options_getter(_getter, a_int=66, a_str='another') # only the options not passed by kwargs should get passed to getter self.assertEqual(set(('a-bool', 'a-float')), called_opts) self.assertFalse(p['a_bool']) self.assertEqual(99.99, p['a_float']) self.assertEqual('another', p['a_str']) self.assertEqual(66, p['a_int']) def test_create_plugin_loader(self): val_a = uuid.uuid4().hex val_b = uuid.uuid4().hex loader = TestSplitLoader() plugin_a = loader.load_from_options(a=val_a) plugin_b = loader.load_from_options(b=val_b) self.assertIsInstance(plugin_a, PluginA) self.assertIsInstance(plugin_b, PluginB) self.assertEqual(val_a, plugin_a.val) self.assertEqual(val_b, plugin_b.val) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/loading/test_session.py0000664000175000017500000000716600000000000026016 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse import uuid from oslo_config import cfg from oslo_config import fixture as config from testtools import matchers from keystoneauth1 import loading from keystoneauth1.tests.unit.loading import utils class ConfLoadingTests(utils.TestCase): GROUP = 'sessiongroup' def setUp(self): super(ConfLoadingTests, self).setUp() self.conf_fixture = self.useFixture(config.Config()) loading.register_session_conf_options(self.conf_fixture.conf, self.GROUP) def config(self, **kwargs): kwargs['group'] = self.GROUP self.conf_fixture.config(**kwargs) def get_session(self, **kwargs): return loading.load_session_from_conf_options(self.conf_fixture.conf, self.GROUP, **kwargs) def test_insecure_timeout(self): self.config(insecure=True, timeout=5) s = self.get_session() self.assertFalse(s.verify) self.assertEqual(5, s.timeout) def test_client_certs(self): cert = '/path/to/certfile' key = '/path/to/keyfile' self.config(certfile=cert, keyfile=key) s = self.get_session() self.assertTrue(s.verify) self.assertEqual((cert, key), s.cert) def test_cacert(self): cafile = '/path/to/cacert' self.config(cafile=cafile) s = self.get_session() self.assertEqual(cafile, s.verify) def test_deprecated(self): def new_deprecated(): return cfg.DeprecatedOpt(uuid.uuid4().hex, group=uuid.uuid4().hex) opt_names = [ 'cafile', 'certfile', 'keyfile', 'insecure', 'timeout', 'collect-timing', 'split-loggers', ] depr = dict([(n, [new_deprecated()]) for n in opt_names]) opts = loading.get_session_conf_options(deprecated_opts=depr) self.assertThat(opt_names, matchers.HasLength(len(opts))) for opt in opts: self.assertIn(depr[opt.name][0], opt.deprecated_opts) class CliLoadingTests(utils.TestCase): def setUp(self): super(CliLoadingTests, self).setUp() self.parser = argparse.ArgumentParser() loading.register_session_argparse_arguments(self.parser) def get_session(self, val, **kwargs): args = self.parser.parse_args(val.split()) return loading.load_session_from_argparse_arguments(args, **kwargs) def test_insecure_timeout(self): s = self.get_session('--insecure --timeout 5.5') self.assertFalse(s.verify) self.assertEqual(5.5, s.timeout) def test_client_certs(self): cert = '/path/to/certfile' key = '/path/to/keyfile' s = self.get_session('--os-cert %s --os-key %s' % (cert, key)) self.assertTrue(s.verify) self.assertEqual((cert, key), s.cert) def test_cacert(self): cacert = '/path/to/cacert' s = self.get_session('--os-cacert %s' % cacert) self.assertEqual(cacert, s.verify) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/loading/test_v3.py0000664000175000017500000004406100000000000024656 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import random import uuid from keystoneauth1 import exceptions from keystoneauth1 import loading from keystoneauth1.tests.unit.loading import utils class V3PasswordTests(utils.TestCase): def setUp(self): super(V3PasswordTests, self).setUp() self.auth_url = uuid.uuid4().hex def create(self, **kwargs): kwargs.setdefault('auth_url', self.auth_url) loader = loading.get_plugin_loader('v3password') return loader.load_from_options(**kwargs) def test_basic(self): username = uuid.uuid4().hex user_domain_id = uuid.uuid4().hex password = uuid.uuid4().hex project_name = uuid.uuid4().hex project_domain_id = uuid.uuid4().hex p = self.create(username=username, user_domain_id=user_domain_id, project_name=project_name, project_domain_id=project_domain_id, password=password) pw_method = p.auth_methods[0] self.assertEqual(username, pw_method.username) self.assertEqual(user_domain_id, pw_method.user_domain_id) self.assertEqual(password, pw_method.password) self.assertEqual(project_name, p.project_name) self.assertEqual(project_domain_id, p.project_domain_id) def test_without_user_domain(self): self.assertRaises(exceptions.OptionError, self.create, username=uuid.uuid4().hex, password=uuid.uuid4().hex) def test_without_project_domain(self): self.assertRaises(exceptions.OptionError, self.create, username=uuid.uuid4().hex, password=uuid.uuid4().hex, user_domain_id=uuid.uuid4().hex, project_name=uuid.uuid4().hex) class TOTPTests(utils.TestCase): def setUp(self): super(TOTPTests, self).setUp() self.auth_url = uuid.uuid4().hex def create(self, **kwargs): kwargs.setdefault('auth_url', self.auth_url) loader = loading.get_plugin_loader('v3totp') return loader.load_from_options(**kwargs) def test_basic(self): username = uuid.uuid4().hex user_domain_id = uuid.uuid4().hex # passcode is 6 digits passcode = ''.join(str(random.randint(0, 9)) for x in range(6)) project_name = uuid.uuid4().hex project_domain_id = uuid.uuid4().hex p = self.create(username=username, user_domain_id=user_domain_id, project_name=project_name, project_domain_id=project_domain_id, passcode=passcode) totp_method = p.auth_methods[0] self.assertEqual(username, totp_method.username) self.assertEqual(user_domain_id, totp_method.user_domain_id) self.assertEqual(passcode, totp_method.passcode) self.assertEqual(project_name, p.project_name) self.assertEqual(project_domain_id, p.project_domain_id) def test_without_user_domain(self): self.assertRaises(exceptions.OptionError, self.create, username=uuid.uuid4().hex, passcode=uuid.uuid4().hex) def test_without_project_domain(self): self.assertRaises(exceptions.OptionError, self.create, username=uuid.uuid4().hex, passcode=uuid.uuid4().hex, user_domain_id=uuid.uuid4().hex, project_name=uuid.uuid4().hex) class OpenIDConnectBaseTests(object): plugin_name = None def setUp(self): super(OpenIDConnectBaseTests, self).setUp() self.auth_url = uuid.uuid4().hex def create(self, **kwargs): kwargs.setdefault('auth_url', self.auth_url) loader = loading.get_plugin_loader(self.plugin_name) return loader.load_from_options(**kwargs) def test_base_options_are_there(self): options = loading.get_plugin_loader(self.plugin_name).get_options() self.assertTrue( set(['client-id', 'client-secret', 'access-token-endpoint', 'access-token-type', 'openid-scope', 'discovery-endpoint']).issubset( set([o.name for o in options])) ) # openid-scope gets renamed into "scope" self.assertIn('scope', [o.dest for o in options]) class OpenIDConnectClientCredentialsTests(OpenIDConnectBaseTests, utils.TestCase): plugin_name = "v3oidcclientcredentials" def test_options(self): options = loading.get_plugin_loader(self.plugin_name).get_options() self.assertTrue( set(['openid-scope']).issubset( set([o.name for o in options])) ) def test_basic(self): access_token_endpoint = uuid.uuid4().hex scope = uuid.uuid4().hex identity_provider = uuid.uuid4().hex protocol = uuid.uuid4().hex scope = uuid.uuid4().hex client_id = uuid.uuid4().hex client_secret = uuid.uuid4().hex oidc = self.create(identity_provider=identity_provider, protocol=protocol, access_token_endpoint=access_token_endpoint, client_id=client_id, client_secret=client_secret, scope=scope) self.assertEqual(scope, oidc.scope) self.assertEqual(identity_provider, oidc.identity_provider) self.assertEqual(protocol, oidc.protocol) self.assertEqual(access_token_endpoint, oidc.access_token_endpoint) self.assertEqual(client_id, oidc.client_id) self.assertEqual(client_secret, oidc.client_secret) class OpenIDConnectPasswordTests(OpenIDConnectBaseTests, utils.TestCase): plugin_name = "v3oidcpassword" def test_options(self): options = loading.get_plugin_loader(self.plugin_name).get_options() self.assertTrue( set(['username', 'password', 'openid-scope']).issubset( set([o.name for o in options])) ) def test_basic(self): access_token_endpoint = uuid.uuid4().hex username = uuid.uuid4().hex password = uuid.uuid4().hex scope = uuid.uuid4().hex identity_provider = uuid.uuid4().hex protocol = uuid.uuid4().hex scope = uuid.uuid4().hex client_id = uuid.uuid4().hex client_secret = uuid.uuid4().hex oidc = self.create(username=username, password=password, identity_provider=identity_provider, protocol=protocol, access_token_endpoint=access_token_endpoint, client_id=client_id, client_secret=client_secret, scope=scope) self.assertEqual(username, oidc.username) self.assertEqual(password, oidc.password) self.assertEqual(scope, oidc.scope) self.assertEqual(identity_provider, oidc.identity_provider) self.assertEqual(protocol, oidc.protocol) self.assertEqual(access_token_endpoint, oidc.access_token_endpoint) self.assertEqual(client_id, oidc.client_id) self.assertEqual(client_secret, oidc.client_secret) class OpenIDConnectAuthCodeTests(OpenIDConnectBaseTests, utils.TestCase): plugin_name = "v3oidcauthcode" def test_options(self): options = loading.get_plugin_loader(self.plugin_name).get_options() self.assertTrue( set(['redirect-uri', 'code']).issubset( set([o.name for o in options])) ) def test_basic(self): access_token_endpoint = uuid.uuid4().hex redirect_uri = uuid.uuid4().hex authorization_code = uuid.uuid4().hex scope = uuid.uuid4().hex identity_provider = uuid.uuid4().hex protocol = uuid.uuid4().hex client_id = uuid.uuid4().hex client_secret = uuid.uuid4().hex oidc = self.create(code=authorization_code, redirect_uri=redirect_uri, identity_provider=identity_provider, protocol=protocol, access_token_endpoint=access_token_endpoint, client_id=client_id, client_secret=client_secret, scope=scope) self.assertEqual(redirect_uri, oidc.redirect_uri) self.assertEqual(authorization_code, oidc.code) self.assertEqual(scope, oidc.scope) self.assertEqual(identity_provider, oidc.identity_provider) self.assertEqual(protocol, oidc.protocol) self.assertEqual(access_token_endpoint, oidc.access_token_endpoint) self.assertEqual(client_id, oidc.client_id) self.assertEqual(client_secret, oidc.client_secret) class OpenIDConnectAccessToken(utils.TestCase): plugin_name = "v3oidcaccesstoken" def setUp(self): super(OpenIDConnectAccessToken, self).setUp() self.auth_url = uuid.uuid4().hex def create(self, **kwargs): kwargs.setdefault('auth_url', self.auth_url) loader = loading.get_plugin_loader(self.plugin_name) return loader.load_from_options(**kwargs) def test_options(self): options = loading.get_plugin_loader(self.plugin_name).get_options() self.assertTrue( set(['access-token']).issubset( set([o.name for o in options])) ) def test_basic(self): access_token = uuid.uuid4().hex identity_provider = uuid.uuid4().hex protocol = uuid.uuid4().hex oidc = self.create(access_token=access_token, identity_provider=identity_provider, protocol=protocol) self.assertEqual(identity_provider, oidc.identity_provider) self.assertEqual(protocol, oidc.protocol) self.assertEqual(access_token, oidc.access_token) class V3TokenlessAuthTests(utils.TestCase): def setUp(self): super(V3TokenlessAuthTests, self).setUp() self.auth_url = uuid.uuid4().hex def create(self, **kwargs): kwargs.setdefault('auth_url', self.auth_url) loader = loading.get_plugin_loader('v3tokenlessauth') return loader.load_from_options(**kwargs) def test_basic(self): domain_id = uuid.uuid4().hex domain_name = uuid.uuid4().hex project_id = uuid.uuid4().hex project_name = uuid.uuid4().hex project_domain_id = uuid.uuid4().hex project_domain_name = uuid.uuid4().hex tla = self.create(domain_id=domain_id, domain_name=domain_name, project_id=project_id, project_name=project_name, project_domain_id=project_domain_id, project_domain_name=project_domain_name) self.assertEqual(domain_id, tla.domain_id) self.assertEqual(domain_name, tla.domain_name) self.assertEqual(project_id, tla.project_id) self.assertEqual(project_name, tla.project_name) self.assertEqual(project_domain_id, tla.project_domain_id) self.assertEqual(project_domain_name, tla.project_domain_name) def test_missing_parameters(self): self.assertRaises(exceptions.OptionError, self.create, domain_id=None) self.assertRaises(exceptions.OptionError, self.create, domain_name=None) self.assertRaises(exceptions.OptionError, self.create, project_id=None) self.assertRaises(exceptions.OptionError, self.create, project_name=None) self.assertRaises(exceptions.OptionError, self.create, project_domain_id=None) self.assertRaises(exceptions.OptionError, self.create, project_domain_name=None) # only when a project_name is provided, project_domain_id will # be use to uniquely identify the project. It's an invalid # option when it's just by itself. self.assertRaises(exceptions.OptionError, self.create, project_domain_id=uuid.uuid4().hex) # only when a project_name is provided, project_domain_name will # be use to uniquely identify the project. It's an invalid # option when it's just by itself. self.assertRaises(exceptions.OptionError, self.create, project_domain_name=uuid.uuid4().hex) self.assertRaises(exceptions.OptionError, self.create, project_name=uuid.uuid4().hex) class V3ApplicationCredentialTests(utils.TestCase): def setUp(self): super(V3ApplicationCredentialTests, self).setUp() self.auth_url = uuid.uuid4().hex def create(self, **kwargs): kwargs.setdefault('auth_url', self.auth_url) loader = loading.get_plugin_loader('v3applicationcredential') return loader.load_from_options(**kwargs) def test_basic(self): id = uuid.uuid4().hex secret = uuid.uuid4().hex app_cred = self.create(application_credential_id=id, application_credential_secret=secret) ac_method = app_cred.auth_methods[0] self.assertEqual(id, ac_method.application_credential_id) self.assertEqual(secret, ac_method.application_credential_secret) def test_with_name(self): name = uuid.uuid4().hex secret = uuid.uuid4().hex username = uuid.uuid4().hex user_domain_id = uuid.uuid4().hex app_cred = self.create(application_credential_name=name, application_credential_secret=secret, username=username, user_domain_id=user_domain_id) ac_method = app_cred.auth_methods[0] self.assertEqual(name, ac_method.application_credential_name) self.assertEqual(secret, ac_method.application_credential_secret) self.assertEqual(username, ac_method.username) self.assertEqual(user_domain_id, ac_method.user_domain_id) def test_without_user_domain(self): self.assertRaises(exceptions.OptionError, self.create, application_credential_name=uuid.uuid4().hex, username=uuid.uuid4().hex, application_credential_secret=uuid.uuid4().hex) def test_without_name_or_id(self): self.assertRaises(exceptions.OptionError, self.create, username=uuid.uuid4().hex, user_domain_id=uuid.uuid4().hex, application_credential_secret=uuid.uuid4().hex) def test_without_secret(self): self.assertRaises(exceptions.OptionError, self.create, application_credential_id=uuid.uuid4().hex, username=uuid.uuid4().hex, user_domain_id=uuid.uuid4().hex) class MultiFactorTests(utils.TestCase): def setUp(self): super(MultiFactorTests, self).setUp() self.auth_url = uuid.uuid4().hex def create(self, **kwargs): kwargs.setdefault('auth_url', self.auth_url) loader = loading.get_plugin_loader('v3multifactor') return loader.load_from_options(**kwargs) def test_password_and_totp(self): username = uuid.uuid4().hex password = uuid.uuid4().hex user_domain_id = uuid.uuid4().hex # passcode is 6 digits passcode = ''.join(str(random.randint(0, 9)) for x in range(6)) project_name = uuid.uuid4().hex project_domain_id = uuid.uuid4().hex p = self.create( auth_methods=['v3password', 'v3totp'], username=username, password=password, user_domain_id=user_domain_id, project_name=project_name, project_domain_id=project_domain_id, passcode=passcode) password_method = p.auth_methods[0] totp_method = p.auth_methods[1] self.assertEqual(username, password_method.username) self.assertEqual(user_domain_id, password_method.user_domain_id) self.assertEqual(password, password_method.password) self.assertEqual(username, totp_method.username) self.assertEqual(user_domain_id, totp_method.user_domain_id) self.assertEqual(passcode, totp_method.passcode) self.assertEqual(project_name, p.project_name) self.assertEqual(project_domain_id, p.project_domain_id) def test_without_methods(self): self.assertRaises(exceptions.OptionError, self.create, username=uuid.uuid4().hex, passcode=uuid.uuid4().hex) def test_without_user_domain_for_password(self): self.assertRaises(exceptions.OptionError, self.create, methods=['v3password'], username=uuid.uuid4().hex, project_name=uuid.uuid4().hex, project_domain_id=uuid.uuid4().hex) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/loading/utils.py0000664000175000017500000000627200000000000024431 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import functools from unittest import mock import uuid from keystoneauth1 import loading from keystoneauth1.loading import base from keystoneauth1 import plugin from keystoneauth1.tests.unit import utils class TestCase(utils.TestCase): GROUP = 'auth' V2PASS = 'v2password' V3TOKEN = 'v3token' a_int = 88 a_float = 88.8 a_bool = False TEST_VALS = {'a_int': a_int, 'a_float': a_float, 'a_bool': a_bool} def assertTestVals(self, plugin, vals=TEST_VALS): for k, v in vals.items(): self.assertEqual(v, plugin[k]) def create_plugin(opts=[], token=None, endpoint=None): class Plugin(plugin.BaseAuthPlugin): def __init__(self, **kwargs): self._data = kwargs def __getitem__(self, key): return self._data[key] def get_token(self, *args, **kwargs): return token def get_endpoint(self, *args, **kwargs): return endpoint class Loader(loading.BaseLoader): @property def plugin_class(self): return Plugin def get_options(self): return opts return Plugin, Loader class BoolType(object): def __eq__(self, other): """Define equiality for many bool types.""" # hack around oslo.config equality comparison return type(self) == type(other) # NOTE: This function is only needed by Python 2. If we get to point where # we don't support Python 2 anymore, this function should be removed. def __ne__(self, other): """Define inequiality for many bool types.""" return not self.__eq__(other) def __call__(self, value): return str(value).lower() in ('1', 'true', 't', 'yes', 'y') INT_DESC = 'test int' FLOAT_DESC = 'test float' BOOL_DESC = 'test bool' STR_DESC = 'test str' STR_DEFAULT = uuid.uuid4().hex MockPlugin, MockLoader = create_plugin( endpoint='http://test', token='aToken', opts=[ loading.Opt('a-int', default=3, type=int, help=INT_DESC), loading.Opt('a-bool', type=BoolType(), help=BOOL_DESC), loading.Opt('a-float', type=float, help=FLOAT_DESC), loading.Opt('a-str', help=STR_DESC, default=STR_DEFAULT), ] ) class MockManager(object): def __init__(self, driver): self.driver = driver def mock_plugin(loader=MockLoader): def _wrapper(f): @functools.wraps(f) def inner(*args, **kwargs): with mock.patch.object(base, 'get_plugin_loader') as m: m.return_value = loader() args = list(args) + [m] return f(*args, **kwargs) return inner return _wrapper ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/matchers.py0000664000175000017500000000622700000000000023462 0ustar00zuulzuul00000000000000# Copyright 2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from lxml import etree from testtools import matchers class XMLEquals(object): """Parses two XML documents from strings and compares the results.""" def __init__(self, expected): self.expected = expected def __str__(self): """Return string representation of xml document info.""" return "%s(%r)" % (self.__class__.__name__, self.expected) def match(self, other): def xml_element_equals(expected_doc, observed_doc): """Test whether two XML documents are equivalent. This is a recursive algorithm that operates on each element in the hierarchy. Siblings are sorted before being checked to account for two semantically equivalent documents where siblings appear in different document order. The sorting algorithm is a little weak in that it could fail for documents where siblings at a given level are the same, but have different children. """ if expected_doc.tag != observed_doc.tag: return False if expected_doc.attrib != observed_doc.attrib: return False def _sorted_children(doc): return sorted(doc.getchildren(), key=lambda el: el.tag) expected_children = _sorted_children(expected_doc) observed_children = _sorted_children(observed_doc) if len(expected_children) != len(observed_children): return False for expected_el, observed_el in zip(expected_children, observed_children): if not xml_element_equals(expected_el, observed_el): return False return True parser = etree.XMLParser(remove_blank_text=True) expected_doc = etree.fromstring(self.expected.strip(), parser) observed_doc = etree.fromstring(other.strip(), parser) if xml_element_equals(expected_doc, observed_doc): return return XMLMismatch(self.expected, other) class XMLMismatch(matchers.Mismatch): def __init__(self, expected, other): self.expected = expected self.other = other def describe(self): def pretty_xml(xml): parser = etree.XMLParser(remove_blank_text=True) doc = etree.fromstring(xml.strip(), parser) return (etree.tostring(doc, encoding='utf-8', pretty_print=True) .decode('utf-8')) return 'expected =\n%s\nactual =\n%s' % ( pretty_xml(self.expected), pretty_xml(self.other)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/oidc_fixtures.py0000664000175000017500000000607200000000000024521 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. UNSCOPED_TOKEN = { "token": { "issued_at": "2014-06-09T09:48:59.643406Z", "extras": {}, "methods": ["oidc"], "expires_at": "2014-06-09T10:48:59.643375Z", "user": { "OS-FEDERATION": { "identity_provider": { "id": "bluepages" }, "protocol": { "id": "oidc" }, "groups": [ {"id": "1764fa5cf69a49a4918131de5ce4af9a"} ] }, "id": "oidc_user%40example.com", "name": "oidc_user@example.com" } } } ACCESS_TOKEN_VIA_PASSWORD_RESP = { "access_token": "z5H1ITZLlJVDHQXqJun", "token_type": "bearer", "expires_in": 3599, "scope": "openid profile", "refresh_token": "DCERsh83IAhu9bhavrp" } ACCESS_TOKEN_VIA_AUTH_GRANT_RESP = { "access_token": "ya29.jgGIjfVrBPWLStWSU3eh8ioE6hG06QQ", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "1/ySXNO9XISBMIgOrJDtdun6zK6XiATCKT", "id_token": "eyJhbGciOiJSUzI1Ni8hOYHuZT8dt_yynmJVhcU" } DISCOVERY_DOCUMENT = { "authorization_endpoint": "https://localhost:8020/oidc/authorize", "claims_supported": [ "sub", "name", "preferred_username", "given_name", "family_name", "middle_name", "nickname", "profile", "picture", "website", "gender", "zoneinfo", "locale", "updated_at", "birthdate", "email", "email_verified", "phone_number", "phone_number_verified", "address" ], "grant_types_supported": [ "authorization_code", "password", ], "introspection_endpoint": "https://localhost:8020/oidc/introspect", "issuer": "https://localhost:8020/oidc/", "jwks_uri": "https://localhost:8020/oidc/jwk", "op_policy_uri": "https://localhost:8020/oidc/about", "op_tos_uri": "https://localhost:8020/oidc/about", "registration_endpoint": "https://localhost:8020/oidc/register", "revocation_endpoint": "https://localhost:8020/oidc/revoke", "service_documentation": "https://localhost:8020/oidc/about", "token_endpoint": "https://localhost:8020/oidc/token", "userinfo_endpoint": "https://localhost:8020/oidc/userinfo", "token_endpoint_auth_methods_supported": [ "client_secret_post", "client_secret_basic", "client_secret_jwt", "private_key_jwt", "none" ], } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/test_betamax_fixture.py0000664000175000017500000001102400000000000026071 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import betamax from betamax import exceptions import testtools from keystoneauth1.fixture import keystoneauth_betamax from keystoneauth1.fixture import serializer from keystoneauth1.fixture import v2 as v2Fixtures from keystoneauth1.identity import v2 from keystoneauth1 import session class TestBetamaxFixture(testtools.TestCase): TEST_USERNAME = 'test_user_name' TEST_PASSWORD = 'test_password' TEST_TENANT_NAME = 'test_tenant_name' TEST_AUTH_URL = 'http://keystoneauth-betamax.test/v2.0/' V2_TOKEN = v2Fixtures.Token(tenant_name=TEST_TENANT_NAME, user_name=TEST_USERNAME) def setUp(self): super(TestBetamaxFixture, self).setUp() self.ksa_betamax_fixture = self.useFixture( keystoneauth_betamax.BetamaxFixture( cassette_name='ksa_betamax_test_cassette', cassette_library_dir='keystoneauth1/tests/unit/data/', record=False)) def _replay_cassette(self): plugin = v2.Password( auth_url=self.TEST_AUTH_URL, password=self.TEST_PASSWORD, username=self.TEST_USERNAME, tenant_name=self.TEST_TENANT_NAME) s = session.Session() s.get_token(auth=plugin) def test_keystoneauth_betamax_fixture(self): self._replay_cassette() def test_replay_of_bad_url_fails(self): plugin = v2.Password( auth_url='http://invalid-auth-url/v2.0/', password=self.TEST_PASSWORD, username=self.TEST_USERNAME, tenant_name=self.TEST_TENANT_NAME) s = session.Session() self.assertRaises(exceptions.BetamaxError, s.get_token, auth=plugin) class TestBetamaxFixtureSerializerBehaviour(testtools.TestCase): """Test the fixture's logic, not its monkey-patching. The setUp method of our BetamaxFixture monkey-patches the function to construct a session. We don't need to test that particular bit of logic here so we do not need to call useFixture in our setUp method. """ @mock.patch.object(betamax.Betamax, 'register_serializer') def test_can_pass_custom_serializer(self, register_serializer): serializer = mock.Mock() serializer.name = 'mocked-serializer' fixture = keystoneauth_betamax.BetamaxFixture( cassette_name='fake', cassette_library_dir='keystoneauth1/tests/unit/data', serializer=serializer, ) register_serializer.assert_called_once_with(serializer) self.assertIs(serializer, fixture.serializer) self.assertEqual('mocked-serializer', fixture.serializer_name) def test_can_pass_serializer_name(self): fixture = keystoneauth_betamax.BetamaxFixture( cassette_name='fake', cassette_library_dir='keystoneauth1/tests/unit/data', serializer_name='json', ) self.assertIsNone(fixture.serializer) self.assertEqual('json', fixture.serializer_name) def test_no_serializer_options_provided(self): fixture = keystoneauth_betamax.BetamaxFixture( cassette_name='fake', cassette_library_dir='keystoneauth1/tests/unit/data', ) self.assertIs(serializer.YamlJsonSerializer, fixture.serializer) self.assertEqual('yamljson', fixture.serializer_name) def test_no_request_matchers_provided(self): fixture = keystoneauth_betamax.BetamaxFixture( cassette_name='fake', cassette_library_dir='keystoneauth1/tests/unit/data', ) self.assertDictEqual({}, fixture.use_cassette_kwargs) def test_request_matchers(self): fixture = keystoneauth_betamax.BetamaxFixture( cassette_name='fake', cassette_library_dir='keystoneauth1/tests/unit/data', request_matchers=['method', 'uri', 'json-body'], ) self.assertDictEqual( {'match_requests_on': ['method', 'uri', 'json-body']}, fixture.use_cassette_kwargs, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/test_betamax_hooks.py0000664000175000017500000001506400000000000025536 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from unittest import mock import betamax from requests import models import testtools try: from requests.packages.urllib3._collections import HTTPHeaderDict except ImportError: from betamax.headers import HTTPHeaderDict from keystoneauth1.fixture import hooks class TestBetamaxHooks(testtools.TestCase): def test_pre_record_hook_v3(self): fixtures_path = 'keystoneauth1/tests/unit/data' with betamax.Betamax.configure() as config: config.before_record(callback=hooks.pre_record_hook) cassette = betamax.cassette.Cassette( 'test_pre_record_hook', 'json', record_mode=None, cassette_library_dir=fixtures_path) # Create a new object to serialize r = models.Response() r.status_code = 200 r.reason = 'OK' r.encoding = 'utf-8' r.headers = {} r.url = 'http://localhost:35357/' # load request and response with open('%s/keystone_v3_sample_response.json' % fixtures_path) as f: response_content = json.loads(f.read()) with open('%s/keystone_v3_sample_request.json' % fixtures_path) as f: request_content = json.loads(f.read()) body_content = { 'body': { 'string': json.dumps(response_content), 'encoding': 'utf-8', } } betamax.util.add_urllib3_response( body_content, r, HTTPHeaderDict({'Accept': 'application/json'})) response = r # Create an associated request r = models.Request() r.method = 'GET' r.url = 'http://localhost:35357/' r.headers = {} r.data = {} response.request = r.prepare() response.request.headers.update( {'User-Agent': 'betamax/test header'} ) response.request.body = json.dumps(request_content) interaction = cassette.save_interaction(response, response.request) # check that all values have been masked response_content = json.loads( interaction.data['response']['body']['string']) self.assertEqual( response_content['token']['expires_at'], u'9999-12-31T23:59:59Z') self.assertEqual( response_content['token']['project']['domain']['id'], u'dummy') self.assertEqual( response_content['token']['user']['domain']['id'], u'dummy') self.assertEqual( response_content['token']['user']['name'], u'dummy') request_content = json.loads( interaction.data['request']['body']['string']) self.assertEqual( request_content['auth']['identity']['password'] ['user']['domain']['id'], u'dummy') self.assertEqual( request_content['auth']['identity']['password'] ['user']['password'], u'********') def test_pre_record_hook_v2(self): fixtures_path = 'keystoneauth1/tests/unit/data' with betamax.Betamax.configure() as config: config.before_record(callback=hooks.pre_record_hook) cassette = betamax.cassette.Cassette( 'test_pre_record_hook', 'json', record_mode=None, cassette_library_dir=fixtures_path) # Create a new object to serialize r = models.Response() r.status_code = 200 r.reason = 'OK' r.encoding = 'utf-8' r.headers = {} r.url = 'http://localhost:35357/' # load request and response with open('%s/keystone_v2_sample_response.json' % fixtures_path) as f: response_content = json.loads(f.read()) with open('%s/keystone_v2_sample_request.json' % fixtures_path) as f: request_content = json.loads(f.read()) body_content = { 'body': { 'string': json.dumps(response_content), 'encoding': 'utf-8', } } betamax.util.add_urllib3_response( body_content, r, HTTPHeaderDict({'Accept': 'application/json'})) response = r # Create an associated request r = models.Request() r.method = 'GET' r.url = 'http://localhost:35357/' r.headers = {} r.data = {} response.request = r.prepare() response.request.headers.update( {'User-Agent': 'betamax/test header'} ) response.request.body = json.dumps(request_content) interaction = cassette.save_interaction(response, response.request) # check that all values have been masked response_content = json.loads( interaction.data['response']['body']['string']) self.assertEqual( response_content['access']['token']['expires'], u'9999-12-31T23:59:59Z') self.assertEqual( response_content['access']['token']['tenant']['name'], u'dummy') self.assertEqual( response_content['access']['user']['name'], u'dummy') request_content = json.loads( interaction.data['request']['body']['string']) self.assertEqual( request_content['auth']['passwordCredentials']['password'], u'********') self.assertEqual( request_content['auth']['passwordCredentials']['username'], u'dummy') self.assertEqual( request_content['auth']['tenantName'], u'dummy') @mock.patch('keystoneauth1.fixture.hooks.mask_fixture_values') def test_pre_record_hook_empty_body(self, mask_fixture_values): interaction = mock.Mock() interaction.data = { 'request': { 'body': { 'encoding': 'utf-8', 'string': '', }, }, 'response': { 'body': { 'encoding': 'utf-8', 'string': '', }, }, } hooks.pre_record_hook(interaction, mock.Mock()) self.assertFalse(mask_fixture_values.called) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/test_betamax_serializer.py0000664000175000017500000000370500000000000026563 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import os import testtools import yaml from keystoneauth1.fixture import serializer class TestBetamaxSerializer(testtools.TestCase): TEST_FILE = os.path.join( os.path.dirname(os.path.abspath(__file__)), 'data', 'ksa_betamax_test_cassette.yaml') TEST_JSON = os.path.join( os.path.dirname(os.path.abspath(__file__)), 'data', 'ksa_serializer_data.json') def setUp(self): super(TestBetamaxSerializer, self).setUp() self.serializer = serializer.YamlJsonSerializer() def test_deserialize(self): data = self.serializer.deserialize(open(self.TEST_FILE, 'r').read()) request = data['http_interactions'][0]['request'] self.assertEqual( 'http://keystoneauth-betamax.test/v2.0/tokens', request['uri']) payload = json.loads(request['body']['string']) self.assertEqual('test_tenant_name', payload['auth']['tenantName']) def test_serialize(self): data = json.loads(open(self.TEST_JSON, 'r').read()) serialized = self.serializer.serialize(data) data = yaml.safe_load(serialized) request = data['http_interactions'][0]['request'] self.assertEqual( 'http://keystoneauth-betamax.test/v2.0/tokens', request['uri']) payload = json.loads(request['body']['string']) self.assertEqual('test_tenant_name', payload['auth']['tenantName']) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/test_discovery.py0000664000175000017500000014406200000000000024722 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import re from unittest import mock from testtools import matchers from keystoneauth1 import adapter from keystoneauth1 import discover from keystoneauth1 import exceptions from keystoneauth1 import fixture from keystoneauth1 import http_basic from keystoneauth1 import noauth from keystoneauth1 import session from keystoneauth1.tests.unit import utils from keystoneauth1 import token_endpoint BASE_HOST = 'http://keystone.example.com' BASE_URL = "%s:5000/" % BASE_HOST UPDATED = '2013-03-06T00:00:00Z' TEST_SERVICE_CATALOG = [{ "endpoints": [{ "adminURL": "%s:8774/v1.0" % BASE_HOST, "region": "RegionOne", "internalURL": "%s://127.0.0.1:8774/v1.0" % BASE_HOST, "publicURL": "%s:8774/v1.0/" % BASE_HOST }], "type": "nova_compat", "name": "nova_compat" }, { "endpoints": [{ "adminURL": "http://nova/novapi/admin", "region": "RegionOne", "internalURL": "http://nova/novapi/internal", "publicURL": "http://nova/novapi/public" }], "type": "compute", "name": "nova" }, { "endpoints": [{ "adminURL": "http://glance/glanceapi/admin", "region": "RegionOne", "internalURL": "http://glance/glanceapi/internal", "publicURL": "http://glance/glanceapi/public" }], "type": "image", "name": "glance" }, { "endpoints": [{ "adminURL": "%s:35357/v2.0" % BASE_HOST, "region": "RegionOne", "internalURL": "%s:5000/v2.0" % BASE_HOST, "publicURL": "%s:5000/v2.0" % BASE_HOST }], "type": "identity", "name": "keystone" }, { "endpoints": [{ "adminURL": "http://swift/swiftapi/admin", "region": "RegionOne", "internalURL": "http://swift/swiftapi/internal", "publicURL": "http://swift/swiftapi/public" }], "type": "object-store", "name": "swift" }] V2_URL = "%sv2.0" % BASE_URL V2_VERSION = fixture.V2Discovery(V2_URL) V2_VERSION.updated_str = UPDATED V2_AUTH_RESPONSE = json.dumps({ "access": { "token": { "expires": "2020-01-01T00:00:10.000123Z", "id": 'fakeToken', "tenant": { "id": '1' }, }, "user": { "id": 'test' }, "serviceCatalog": TEST_SERVICE_CATALOG, }, }) V3_URL = "%sv3" % BASE_URL V3_VERSION = fixture.V3Discovery(V3_URL) V3_MEDIA_TYPES = V3_VERSION.media_types V3_VERSION.updated_str = UPDATED V3_AUTH_RESPONSE = json.dumps({ "token": { "methods": [ "token", "password" ], "expires_at": "2020-01-01T00:00:10.000123Z", "project": { "domain": { "id": '1', "name": 'test-domain' }, "id": '1', "name": 'test-project' }, "user": { "domain": { "id": '1', "name": 'test-domain' }, "id": '1', "name": 'test-user' }, "issued_at": "2013-05-29T16:55:21.468960Z", }, }) CINDER_EXAMPLES = { "versions": [ { "status": discover.Status.CURRENT, "updated": "2012-01-04T11:33:21Z", "id": "v1.0", "links": [ { "href": "%sv1/" % BASE_URL, "rel": "self" } ] }, { "status": discover.Status.CURRENT, "updated": "2012-11-21T11:33:21Z", "id": "v2.0", "links": [ { "href": "%sv2/" % BASE_URL, "rel": "self" } ] }, { "status": discover.Status.CURRENT, "updated": "2012-11-21T11:33:21Z", "id": "v3.0", "version": "3.27", "min_version": "3.0", "next_min_version": "3.4", "not_before": "2019-12-31", "links": [ { "href": BASE_URL, "rel": "collection" }, { "href": "%sv3/" % BASE_URL, "rel": "self" } ] } ] } GLANCE_EXAMPLES = { "versions": [ { "status": discover.Status.CURRENT, "id": "v2.2", "links": [ { "href": "%sv2/" % BASE_URL, "rel": "self" } ] }, { "status": discover.Status.SUPPORTED, "id": "v2.1", "links": [ { "href": "%sv2/" % BASE_URL, "rel": "self" } ] }, { "status": discover.Status.SUPPORTED, "id": "v2.0", "links": [ { "href": "%sv2/" % BASE_URL, "rel": "self" } ] }, { "status": discover.Status.CURRENT, "id": "v1.1", "links": [ { "href": "%sv1/" % BASE_URL, "rel": "self" } ] }, { "status": discover.Status.SUPPORTED, "id": "v1.0", "links": [ { "href": "%sv1/" % BASE_URL, "rel": "self" } ] } ] } def _create_version_list(versions): return {'versions': {'values': versions}} def _create_single_version(version): return {'version': version} V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION]) V2_VERSION_LIST = _create_version_list([V2_VERSION]) V3_VERSION_ENTRY = _create_single_version(V3_VERSION) V2_VERSION_ENTRY = _create_single_version(V2_VERSION) class CatalogHackTests(utils.TestCase): TEST_URL = 'http://keystone.server:5000/v2.0' OTHER_URL = 'http://other.server:5000/path' IDENTITY = 'identity' BASE_URL = 'http://keystone.server:5000/' V2_URL = BASE_URL + 'v2.0' V3_URL = BASE_URL + 'v3' def setUp(self): super(CatalogHackTests, self).setUp() self.hacks = discover._VersionHacks() self.hacks.add_discover_hack(self.IDENTITY, re.compile('/v2.0/?$'), '/') def test_version_hacks(self): self.assertEqual(self.BASE_URL, self.hacks.get_discover_hack(self.IDENTITY, self.V2_URL)) self.assertEqual(self.BASE_URL, self.hacks.get_discover_hack(self.IDENTITY, self.V2_URL + '/')) self.assertEqual(self.OTHER_URL, self.hacks.get_discover_hack(self.IDENTITY, self.OTHER_URL)) def test_ignored_non_service_type(self): self.assertEqual(self.V2_URL, self.hacks.get_discover_hack('other', self.V2_URL)) class DiscoverUtils(utils.TestCase): def test_version_number(self): def assertVersion(out, inp): self.assertEqual(out, discover.normalize_version_number(inp)) def versionRaises(inp): self.assertRaises(TypeError, discover.normalize_version_number, inp) assertVersion((1, 2), 'v1.2') assertVersion((11, 0), 'v11') assertVersion((1, 2), '1.2') assertVersion((1, 5, 1), '1.5.1') assertVersion((1, 0), '1') assertVersion((1, 0), 1) assertVersion((5, 2), 5.2) assertVersion((3, 20), '3.20') assertVersion((6, 1), (6, 1)) assertVersion((1, 40), [1, 40]) assertVersion((1, 0), (1,)) assertVersion((1, 0), ['1']) assertVersion((discover.LATEST, discover.LATEST), 'latest') assertVersion((discover.LATEST, discover.LATEST), ['latest']) assertVersion((discover.LATEST, discover.LATEST), discover.LATEST) assertVersion((discover.LATEST, discover.LATEST), (discover.LATEST,)) assertVersion((10, discover.LATEST), '10.latest') assertVersion((10, discover.LATEST), (10, 'latest')) assertVersion((10, discover.LATEST), (10, discover.LATEST)) versionRaises(None) versionRaises('hello') versionRaises('1.a') versionRaises('vacuum') versionRaises('') versionRaises(('1', 'a')) def test_version_args(self): """Validate _normalize_version_args.""" def assert_min_max(in_ver, in_min, in_max, in_type, out_min, out_max): self.assertEqual( (out_min, out_max), discover._normalize_version_args( in_ver, in_min, in_max, service_type=in_type)) def normalize_raises(ver, min, max, in_type): self.assertRaises( ValueError, discover._normalize_version_args, ver, min, max, service_type=in_type) assert_min_max(None, None, None, None, None, None) assert_min_max(None, None, 'v1.2', None, None, (1, 2)) assert_min_max(None, 'v1.2', 'latest', None, (1, 2), (discover.LATEST, discover.LATEST)) assert_min_max(None, 'v1.2', '1.6', None, (1, 2), (1, 6)) assert_min_max(None, 'v1.2', '1.latest', None, (1, 2), (1, discover.LATEST)) assert_min_max(None, 'latest', 'latest', None, (discover.LATEST, discover.LATEST), (discover.LATEST, discover.LATEST)) assert_min_max(None, 'latest', None, None, (discover.LATEST, discover.LATEST), (discover.LATEST, discover.LATEST)) assert_min_max(None, (1, 2), None, None, (1, 2), (discover.LATEST, discover.LATEST)) assert_min_max('', ('1', '2'), (1, 6), None, (1, 2), (1, 6)) assert_min_max(None, ('1', '2'), (1, discover.LATEST), None, (1, 2), (1, discover.LATEST)) assert_min_max('v1.2', '', None, None, (1, 2), (1, discover.LATEST)) assert_min_max('1.latest', None, '', None, (1, discover.LATEST), (1, discover.LATEST)) assert_min_max('v1', None, None, None, (1, 0), (1, discover.LATEST)) assert_min_max('latest', None, None, None, (discover.LATEST, discover.LATEST), (discover.LATEST, discover.LATEST)) assert_min_max(None, None, 'latest', 'volumev2', (2, 0), (2, discover.LATEST)) assert_min_max(None, None, None, 'volumev2', (2, 0), (2, discover.LATEST)) normalize_raises('v1', 'v2', None, None) normalize_raises('v1', None, 'v2', None) normalize_raises(None, 'latest', 'v1', None) normalize_raises(None, 'v1.2', 'v1.1', None) normalize_raises(None, 'v1.2', 1, None) normalize_raises('v2', None, None, 'volumev3') normalize_raises('v3', None, None, 'volumev2') normalize_raises(None, 'v2', None, 'volumev3') normalize_raises(None, None, 'v3', 'volumev2') def test_version_to_string(self): def assert_string(out, inp): self.assertEqual(out, discover.version_to_string(inp)) assert_string('latest', (discover.LATEST,)) assert_string('latest', (discover.LATEST, discover.LATEST)) assert_string('latest', (discover.LATEST, discover.LATEST, discover.LATEST)) assert_string('1', (1,)) assert_string('1.2', (1, 2)) assert_string('1.latest', (1, discover.LATEST)) def test_version_between(self): def good(minver, maxver, cand): self.assertTrue(discover.version_between(minver, maxver, cand)) def bad(minver, maxver, cand): self.assertFalse(discover.version_between(minver, maxver, cand)) def exc(excls, minver, maxver, cand): self.assertRaises(excls, discover.version_between, minver, maxver, cand) # candidate required exc(ValueError, (1, 0), (1, 0), None) exc(ValueError, 'v1.0', '1.0', '') # malformed candidate exc(TypeError, None, None, 'bogus') exc(TypeError, None, None, (1, 'two')) # malformed min_version exc(TypeError, 'bogus', None, (1, 0)) exc(TypeError, (1, 'two'), None, (1, 0)) # malformed max_version exc(TypeError, None, 'bogus', (1, 0)) exc(TypeError, None, (1, 'two'), (1, 0)) # fail on minimum bad((2, 4), None, (1, 55)) bad('v2.4', '', '2.3') bad('latest', None, (2, 3000)) bad((2, discover.LATEST), '', 'v2.3000') bad((2, 3000), '', (1, discover.LATEST)) bad('latest', None, 'v1000.latest') # fail on maximum bad(None, (2, 4), (2, 5)) bad('', 'v2.4', '2.5') bad(None, (2, discover.LATEST), (3, 0)) bad('', '2000.latest', 'latest') # candidate matches a bound good((1, 0), (1, 0), (1, 0)) good('1.0', '2.9', '1.0') good('v1.0', 'v2.9', 'v2.9') # properly in between good((1, 0), (1, 10), (1, 2)) good('1', '2', '1.2') # no lower bound good(None, (2, 5), (2, 3)) # no upper bound good('2.5', '', '2.6') # no bounds at all good('', '', 'v1') good(None, None, (999, 999)) good(None, None, 'latest') # Various good 'latest' scenarios good((discover.LATEST, discover.LATEST), (discover.LATEST, discover.LATEST), (discover.LATEST, discover.LATEST)) good((discover.LATEST, discover.LATEST), None, (discover.LATEST, discover.LATEST)) good('', 'latest', 'latest') good('2.latest', '3.latest', '3.0') good('2.latest', None, (55, 66)) good(None, '3.latest', '3.9999') class VersionDataTests(utils.TestCase): def setUp(self): super(VersionDataTests, self).setUp() self.session = session.Session() def test_version_data_basics(self): examples = {'keystone': V3_VERSION_LIST, 'cinder': CINDER_EXAMPLES, 'glance': GLANCE_EXAMPLES} for path, data in examples.items(): url = "%s%s" % (BASE_URL, path) mock = self.requests_mock.get(url, status_code=300, json=data) disc = discover.Discover(self.session, url) raw_data = disc.raw_version_data() clean_data = disc.version_data() for v in raw_data: for n in ('id', 'status', 'links'): msg = '%s missing from %s version data' % (n, path) self.assertThat(v, matchers.Annotate(msg, matchers.Contains(n))) for v in clean_data: for n in ('version', 'url', 'raw_status'): msg = '%s missing from %s version data' % (n, path) self.assertThat(v, matchers.Annotate(msg, matchers.Contains(n))) self.assertTrue(mock.called_once) def test_version_data_override_version_url(self): # if the request url is versioned already, just return it. self.requests_mock.get( V3_URL, status_code=200, json={'version': fixture.V3Discovery('http://override/identity/v3') } ) disc = discover.Discover(self.session, V3_URL) version_data = disc.version_data() for v in version_data: self.assertEqual(v['version'], (3, 0)) self.assertEqual(v['status'], discover.Status.CURRENT) self.assertEqual(v['raw_status'], 'stable') self.assertEqual(v['url'], V3_URL) # if the request url is not versioned, just add version info to it.( # do not changed the url's netloc or path) self.requests_mock.get( BASE_URL, status_code=200, json={'version': fixture.V3Discovery('http://override/identity/v3') } ) disc = discover.Discover(self.session, BASE_URL) version_data = disc.version_data() for v in version_data: self.assertEqual(v['version'], (3, 0)) self.assertEqual(v['status'], discover.Status.CURRENT) self.assertEqual(v['raw_status'], 'stable') self.assertEqual(v['url'], V3_URL) def test_version_data_unknown(self): discovery_fixture = fixture.V3Discovery(V3_URL) discovery_fixture.status = 'hungry' discovery_doc = _create_single_version(discovery_fixture) self.requests_mock.get(V3_URL, status_code=200, json=discovery_doc) disc = discover.Discover(self.session, V3_URL) clean_data = disc.version_data(allow_unknown=True) self.assertEqual(discover.Status.UNKNOWN, clean_data[0]['status']) def test_version_data_individual(self): mock = self.requests_mock.get(V3_URL, status_code=200, json=V3_VERSION_ENTRY) disc = discover.Discover(self.session, V3_URL) raw_data = disc.raw_version_data() clean_data = disc.version_data() for v in raw_data: self.assertEqual(v['id'], 'v3.0') self.assertEqual(v['status'], 'stable') self.assertIn('media-types', v) self.assertIn('links', v) for v in clean_data: self.assertEqual(v['version'], (3, 0)) self.assertEqual(v['status'], discover.Status.CURRENT) self.assertEqual(v['raw_status'], 'stable') self.assertEqual(v['url'], V3_URL) self.assertTrue(mock.called_once) def test_version_data_legacy_ironic_no_override(self): """Validate detection of legacy Ironic microversion ranges.""" ironic_url = 'https://bare-metal.example.com/v1/' self.requests_mock.get( ironic_url, status_code=200, json={ 'id': 'v1', 'links': [{ "href": ironic_url, "rel": "self"}]}, headers={ 'X-OpenStack-Ironic-API-Minimum-Version': '1.3', 'X-OpenStack-Ironic-API-Maximum-Version': '1.21', }) plugin = noauth.NoAuth() a = adapter.Adapter( self.session, auth=plugin, service_type='baremetal') self.assertIsNone(a.get_api_major_version()) def test_version_data_ironic_microversions(self): """Validate detection of Ironic microversion ranges.""" ironic_url = 'https://bare-metal.example.com/v1/' self.requests_mock.get( ironic_url, status_code=200, json={ 'id': 'v1', 'version': { 'id': 'v1', 'links': [{ "href": ironic_url, "rel": "self"}], 'version': '1.40', 'min_version': '1.10', 'status': 'CURRENT', }, 'links': [{ "href": ironic_url, "rel": "self"}], }, # Keep headers so we can verify that body trumps headers headers={ 'X-OpenStack-Ironic-API-Minimum-Version': '1.3', 'X-OpenStack-Ironic-API-Maximum-Version': '1.21', }) self.assertEqual( [ { 'collection': None, 'version': (1, 0), 'url': ironic_url, 'status': discover.Status.CURRENT, 'raw_status': discover.Status.CURRENT, 'min_microversion': (1, 10), 'max_microversion': (1, 40), 'next_min_version': None, 'not_before': None, }, ], discover.Discover(self.session, ironic_url).version_data()) def test_version_data_legacy_ironic_microversions(self): """Validate detection of legacy Ironic microversion ranges.""" ironic_url = 'https://bare-metal.example.com/v1/' self.requests_mock.get( ironic_url, status_code=200, json={ 'id': 'v1', 'links': [{ "href": ironic_url, "rel": "self"}]}, headers={ 'X-OpenStack-Ironic-API-Minimum-Version': '1.3', 'X-OpenStack-Ironic-API-Maximum-Version': '1.21', }) self.assertEqual( [ { 'collection': None, 'version': (1, 0), 'url': ironic_url, 'status': discover.Status.CURRENT, 'raw_status': discover.Status.CURRENT, 'min_microversion': (1, 3), 'max_microversion': (1, 21), 'next_min_version': None, 'not_before': None, }, ], discover.Discover(self.session, ironic_url).version_data()) def test_version_data_microversions(self): """Validate [min_|max_]version conversion to {min|max}_microversion.""" def setup_mock(versions_in): # Set up the test data with the input version data jsondata = { "versions": [ dict( { "status": discover.Status.CURRENT, "id": "v2.2", "links": [ { "href": V3_URL, "rel": "self" } ] }, **versions_in ) ] } self.requests_mock.get( V3_URL, status_code=200, json=jsondata) def test_ok(versions_in, versions_out): setup_mock(versions_in) # Ensure the output contains the expected microversions self.assertEqual( [ dict( { 'collection': None, 'version': (2, 2), 'url': V3_URL, 'status': discover.Status.CURRENT, 'raw_status': discover.Status.CURRENT, }, **versions_out ) ], discover.Discover(self.session, V3_URL).version_data()) def test_exc(versions_in): setup_mock(versions_in) # Ensure TypeError is raised self.assertRaises( TypeError, discover.Discover(self.session, V3_URL).version_data) # no version info in input test_ok({}, {'min_microversion': None, 'max_microversion': None, 'next_min_version': None, 'not_before': None}) # version => max_microversion test_ok({'version': '2.2'}, {'min_microversion': None, 'max_microversion': (2, 2), 'next_min_version': None, 'not_before': None}) # max_version supersedes version (even if malformed). min_version & # normalization. test_ok({'min_version': '2', 'version': 'foo', 'max_version': '2.2'}, {'min_microversion': (2, 0), 'max_microversion': (2, 2), 'next_min_version': None, 'not_before': None}) # Edge case: min/max_version ignored if present but "empty"; version # used for max_microversion. test_ok({'min_version': '', 'version': '2.1', 'max_version': ''}, {'min_microversion': None, 'max_microversion': (2, 1), 'next_min_version': None, 'not_before': None}) # next_min_version set test_ok({'min_version': '2', 'max_version': '2.2', 'next_min_version': '2.1', 'not_before': '2019-07-01'}, {'min_microversion': (2, 0), 'max_microversion': (2, 2), 'next_min_version': (2, 1), 'not_before': '2019-07-01'}) # Badly-formatted min_version test_exc({'min_version': 'foo', 'max_version': '2.1'}) # Badly-formatted max_version test_exc({'min_version': '2.1', 'max_version': 'foo'}) # Badly-formatted version (when max_version omitted) test_exc({'min_version': '2.1', 'version': 'foo'}) # Badly-formatted next_min_version test_exc({'next_min_version': 'bogus', 'not_before': '2019-07-01'}) def test_endpoint_data_noauth_discover(self): mock = self.requests_mock.get( BASE_URL, status_code=200, json=V3_VERSION_LIST) self.requests_mock.get( V3_URL, status_code=200, json=V3_VERSION_ENTRY) plugin = noauth.NoAuth(endpoint=BASE_URL) data = plugin.get_endpoint_data(self.session) self.assertEqual(data.api_version, (3, 0)) self.assertEqual(data.url, V3_URL) self.assertEqual(plugin.get_api_major_version(self.session), (3, 0)) self.assertEqual(plugin.get_endpoint(self.session), BASE_URL) self.assertTrue(mock.called_once) def test_endpoint_data_noauth_versioned_discover(self): self.requests_mock.get( BASE_URL, status_code=200, json=V3_VERSION_LIST) self.requests_mock.get( V3_URL, status_code=200, json=V3_VERSION_ENTRY) plugin = noauth.NoAuth(endpoint=V3_URL) data = plugin.get_endpoint_data(self.session) self.assertEqual(data.api_version, (3, 0)) self.assertEqual(data.url, V3_URL) self.assertEqual(plugin.get_api_major_version(self.session), (3, 0)) self.assertEqual(plugin.get_endpoint(self.session), V3_URL) def test_endpoint_data_noauth_no_discover(self): plugin = noauth.NoAuth(endpoint=V3_URL) data = plugin.get_endpoint_data( self.session, discover_versions=False) self.assertEqual(data.api_version, (3, 0)) self.assertEqual(data.url, V3_URL) self.assertEqual(plugin.get_api_major_version(self.session), (3, 0)) self.assertEqual(plugin.get_endpoint(self.session), V3_URL) def test_endpoint_data_noauth_override_no_discover(self): plugin = noauth.NoAuth() data = plugin.get_endpoint_data( self.session, endpoint_override=V3_URL, discover_versions=False) self.assertEqual(data.api_version, (3, 0)) self.assertEqual(data.url, V3_URL) self.assertEqual( plugin.get_endpoint(self.session, endpoint_override=V3_URL), V3_URL) def test_endpoint_data_http_basic_discover(self): self.requests_mock.get( BASE_URL, status_code=200, json=V3_VERSION_LIST) self.requests_mock.get( V3_URL, status_code=200, json=V3_VERSION_ENTRY) plugin = http_basic.HTTPBasicAuth(endpoint=V3_URL) data = plugin.get_endpoint_data(self.session) self.assertEqual(data.api_version, (3, 0)) self.assertEqual(data.url, V3_URL) self.assertEqual(plugin.get_api_major_version(self.session), (3, 0)) self.assertEqual(plugin.get_endpoint(self.session), V3_URL) def test_endpoint_data_http_basic_no_discover(self): plugin = http_basic.HTTPBasicAuth(endpoint=V3_URL) data = plugin.get_endpoint_data( self.session, discover_versions=False) self.assertEqual(data.api_version, (3, 0)) self.assertEqual(data.url, V3_URL) self.assertEqual(plugin.get_api_major_version(self.session), (3, 0)) self.assertEqual(plugin.get_endpoint(self.session), V3_URL) def test_endpoint_data_http_basic_override_no_discover(self): plugin = http_basic.HTTPBasicAuth() data = plugin.get_endpoint_data( self.session, endpoint_override=V3_URL, discover_versions=False) self.assertEqual(data.api_version, (3, 0)) self.assertEqual(data.url, V3_URL) self.assertEqual( plugin.get_api_major_version( self.session, endpoint_override=V3_URL), (3, 0)) self.assertEqual( plugin.get_endpoint(self.session, endpoint_override=V3_URL), V3_URL) def test_endpoint_data_noauth_adapter(self): self.requests_mock.get( BASE_URL, status_code=200, json=V3_VERSION_LIST) self.requests_mock.get( V3_URL, status_code=200, json=V3_VERSION_ENTRY) client = adapter.Adapter( session.Session(noauth.NoAuth()), endpoint_override=BASE_URL) data = client.get_endpoint_data() self.assertEqual(data.api_version, (3, 0)) self.assertEqual(data.url, V3_URL) self.assertEqual(client.get_api_major_version(), (3, 0)) self.assertEqual(client.get_endpoint(), BASE_URL) def test_endpoint_data_noauth_versioned_adapter(self): mock = self.requests_mock.get( V3_URL, status_code=200, json=V3_VERSION_ENTRY) client = adapter.Adapter( session.Session(noauth.NoAuth()), endpoint_override=V3_URL) data = client.get_endpoint_data() self.assertEqual(data.api_version, (3, 0)) self.assertEqual(data.url, V3_URL) self.assertEqual(client.get_api_major_version(), (3, 0)) self.assertEqual(client.get_endpoint(), V3_URL) self.assertTrue(mock.called_once) def test_endpoint_data_token_endpoint_discover(self): mock = self.requests_mock.get( V3_URL, status_code=200, json=V3_VERSION_ENTRY) plugin = token_endpoint.Token(endpoint=V3_URL, token='bogus') data = plugin.get_endpoint_data(self.session) self.assertEqual(data.api_version, (3, 0)) self.assertEqual(data.url, V3_URL) self.assertEqual(plugin.get_api_major_version(self.session), (3, 0)) self.assertEqual(plugin.get_endpoint(self.session), V3_URL) self.assertTrue(mock.called_once) def test_endpoint_data_token_endpoint_no_discover(self): plugin = token_endpoint.Token(endpoint=V3_URL, token='bogus') data = plugin.get_endpoint_data(self.session, discover_versions=False) self.assertEqual(data.api_version, (3, 0)) self.assertEqual(data.url, V3_URL) self.assertEqual(plugin.get_api_major_version(self.session), (3, 0)) self.assertEqual(plugin.get_endpoint(self.session), V3_URL) def test_endpoint_data_token_endpoint_adapter(self): mock = self.requests_mock.get( V3_URL, status_code=200, json=V3_VERSION_ENTRY) plugin = token_endpoint.Token(endpoint=V3_URL, token='bogus') client = adapter.Adapter(session.Session(plugin)) data = client.get_endpoint_data() self.assertEqual(data.api_version, (3, 0)) self.assertEqual(data.url, V3_URL) self.assertEqual(client.get_api_major_version(), (3, 0)) self.assertEqual(client.get_endpoint(), V3_URL) self.assertTrue(mock.called_once) def test_data_for_url(self): mock = self.requests_mock.get(V3_URL, status_code=200, json=V3_VERSION_ENTRY) disc = discover.Discover(self.session, V3_URL) for url in (V3_URL, V3_URL + '/'): data = disc.versioned_data_for(url=url) self.assertEqual(data['version'], (3, 0)) self.assertEqual(data['raw_status'], 'stable') self.assertEqual(data['url'], V3_URL) self.assertTrue(mock.called_once) def test_data_for_no_version(self): mock = self.requests_mock.get(V3_URL, status_code=200, json=V3_VERSION_ENTRY) disc = discover.Discover(self.session, V3_URL) data = disc.versioned_data_for() self.assertEqual(data['version'], (3, 0)) self.assertEqual(data['raw_status'], 'stable') self.assertEqual(data['url'], V3_URL) self.assertRaises(TypeError, disc.data_for, version=None) self.assertTrue(mock.called_once) def test_keystone_version_data(self): mock = self.requests_mock.get(BASE_URL, status_code=300, json=V3_VERSION_LIST) disc = discover.Discover(self.session, BASE_URL) raw_data = disc.raw_version_data() clean_data = disc.version_data() self.assertEqual(2, len(raw_data)) self.assertEqual(2, len(clean_data)) for v in raw_data: self.assertIn(v['id'], ('v2.0', 'v3.0')) self.assertEqual(v['updated'], UPDATED) self.assertEqual(v['status'], 'stable') if v['id'] == 'v3.0': self.assertEqual(v['media-types'], V3_MEDIA_TYPES) for v in clean_data: self.assertIn(v['version'], ((2, 0), (3, 0))) self.assertEqual(v['raw_status'], 'stable') valid_v3_versions = ( disc.data_for('v3.0'), disc.data_for('3.latest'), disc.data_for('latest'), disc.versioned_data_for(min_version='v3.0', max_version='v3.latest'), disc.versioned_data_for(min_version='3'), disc.versioned_data_for(min_version='3.latest'), disc.versioned_data_for(min_version='latest'), disc.versioned_data_for(min_version='3.latest', max_version='latest'), disc.versioned_data_for(min_version='latest', max_version='latest'), disc.versioned_data_for(min_version=2), disc.versioned_data_for(min_version='2.latest')) for version in valid_v3_versions: self.assertEqual((3, 0), version['version']) self.assertEqual('stable', version['raw_status']) self.assertEqual(V3_URL, version['url']) valid_v2_versions = ( disc.data_for(2), disc.data_for('2.latest'), disc.versioned_data_for(min_version=2, max_version=(2, discover.LATEST)), disc.versioned_data_for(min_version='2.latest', max_version='2.latest')) for version in valid_v2_versions: self.assertEqual((2, 0), version['version']) self.assertEqual('stable', version['raw_status']) self.assertEqual(V2_URL, version['url']) self.assertIsNone(disc.url_for('v4')) self.assertIsNone(disc.versioned_url_for( min_version='v4', max_version='v4.latest')) self.assertEqual(V3_URL, disc.url_for('v3')) self.assertEqual(V3_URL, disc.versioned_url_for( min_version='v3', max_version='v3.latest')) self.assertEqual(V2_URL, disc.url_for('v2')) self.assertEqual(V2_URL, disc.versioned_url_for( min_version='v2', max_version='v2.latest')) self.assertTrue(mock.called_once) def test_cinder_version_data(self): mock = self.requests_mock.get(BASE_URL, status_code=300, json=CINDER_EXAMPLES) disc = discover.Discover(self.session, BASE_URL) raw_data = disc.raw_version_data() clean_data = disc.version_data() self.assertEqual(3, len(raw_data)) for v in raw_data: self.assertEqual(v['status'], discover.Status.CURRENT) if v['id'] == 'v1.0': self.assertEqual(v['updated'], '2012-01-04T11:33:21Z') elif v['id'] == 'v2.0': self.assertEqual(v['updated'], '2012-11-21T11:33:21Z') elif v['id'] == 'v3.0': self.assertEqual(v['updated'], '2012-11-21T11:33:21Z') else: self.fail("Invalid version found") v1_url = "%sv1/" % BASE_URL v2_url = "%sv2/" % BASE_URL v3_url = "%sv3/" % BASE_URL self.assertEqual(clean_data, [ { 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'version': (1, 0), 'url': v1_url, 'status': discover.Status.CURRENT, 'raw_status': discover.Status.CURRENT, }, { 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'version': (2, 0), 'url': v2_url, 'status': discover.Status.CURRENT, 'raw_status': discover.Status.CURRENT, }, { 'collection': BASE_URL, 'max_microversion': (3, 27), 'min_microversion': (3, 0), 'next_min_version': (3, 4), 'not_before': u'2019-12-31', 'version': (3, 0), 'url': v3_url, 'status': discover.Status.CURRENT, 'raw_status': discover.Status.CURRENT, }, ]) for version in (disc.data_for('v2.0'), disc.versioned_data_for(min_version='v2.0', max_version='v2.latest')): self.assertEqual((2, 0), version['version']) self.assertEqual(discover.Status.CURRENT, version['raw_status']) self.assertEqual(v2_url, version['url']) for version in (disc.data_for(1), disc.versioned_data_for( min_version=(1,), max_version=(1, discover.LATEST))): self.assertEqual((1, 0), version['version']) self.assertEqual(discover.Status.CURRENT, version['raw_status']) self.assertEqual(v1_url, version['url']) self.assertIsNone(disc.url_for('v4')) self.assertIsNone(disc.versioned_url_for(min_version='v4', max_version='v4.latest')) self.assertEqual(v3_url, disc.url_for('v3')) self.assertEqual(v3_url, disc.versioned_url_for( min_version='v3', max_version='v3.latest')) self.assertEqual(v2_url, disc.url_for('v2')) self.assertEqual(v2_url, disc.versioned_url_for( min_version='v2', max_version='v2.latest')) self.assertEqual(v1_url, disc.url_for('v1')) self.assertEqual(v1_url, disc.versioned_url_for( min_version='v1', max_version='v1.latest')) self.assertTrue(mock.called_once) def test_glance_version_data(self): mock = self.requests_mock.get(BASE_URL, status_code=200, json=GLANCE_EXAMPLES) disc = discover.Discover(self.session, BASE_URL) raw_data = disc.raw_version_data() clean_data = disc.version_data() self.assertEqual(5, len(raw_data)) for v in raw_data: if v['id'] in ('v2.2', 'v1.1'): self.assertEqual(v['status'], discover.Status.CURRENT) elif v['id'] in ('v2.1', 'v2.0', 'v1.0'): self.assertEqual(v['status'], discover.Status.SUPPORTED) else: self.fail("Invalid version found") v1_url = '%sv1/' % BASE_URL v2_url = '%sv2/' % BASE_URL self.assertEqual(clean_data, [ { 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'version': (1, 0), 'url': v1_url, 'status': discover.Status.SUPPORTED, 'raw_status': discover.Status.SUPPORTED, }, { 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'version': (1, 1), 'url': v1_url, 'status': discover.Status.CURRENT, 'raw_status': discover.Status.CURRENT, }, { 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'version': (2, 0), 'url': v2_url, 'status': discover.Status.SUPPORTED, 'raw_status': discover.Status.SUPPORTED, }, { 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'version': (2, 1), 'url': v2_url, 'status': discover.Status.SUPPORTED, 'raw_status': discover.Status.SUPPORTED, }, { 'collection': None, 'max_microversion': None, 'min_microversion': None, 'next_min_version': None, 'not_before': None, 'version': (2, 2), 'url': v2_url, 'status': discover.Status.CURRENT, 'raw_status': discover.Status.CURRENT, }, ]) for ver in (2, 2.1, 2.2): for version in (disc.data_for(ver), disc.versioned_data_for( min_version=ver, max_version=(2, discover.LATEST))): self.assertEqual((2, 2), version['version']) self.assertEqual( discover.Status.CURRENT, version['raw_status'] ) self.assertEqual(v2_url, version['url']) self.assertEqual(v2_url, disc.url_for(ver)) for ver in (1, 1.1): for version in (disc.data_for(ver), disc.versioned_data_for( min_version=ver, max_version=(1, discover.LATEST))): self.assertEqual((1, 1), version['version']) self.assertEqual( discover.Status.CURRENT, version['raw_status'] ) self.assertEqual(v1_url, version['url']) self.assertEqual(v1_url, disc.url_for(ver)) self.assertIsNone(disc.url_for('v3')) self.assertIsNone(disc.versioned_url_for(min_version='v3', max_version='v3.latest')) self.assertIsNone(disc.url_for('v2.3')) self.assertIsNone(disc.versioned_url_for(min_version='v2.3', max_version='v2.latest')) self.assertTrue(mock.called_once) def test_allow_deprecated(self): status = 'deprecated' version_list = [{'id': 'v3.0', 'links': [{'href': V3_URL, 'rel': 'self'}], 'media-types': V3_MEDIA_TYPES, 'status': status, 'updated': UPDATED}] self.requests_mock.get(BASE_URL, json={'versions': version_list}) disc = discover.Discover(self.session, BASE_URL) # deprecated is allowed by default versions = disc.version_data(allow_deprecated=False) self.assertEqual(0, len(versions)) versions = disc.version_data(allow_deprecated=True) self.assertEqual(1, len(versions)) self.assertEqual(status, versions[0]['raw_status']) self.assertEqual(V3_URL, versions[0]['url']) self.assertEqual((3, 0), versions[0]['version']) def test_allow_experimental(self): status = 'experimental' version_list = [{'id': 'v3.0', 'links': [{'href': V3_URL, 'rel': 'self'}], 'media-types': V3_MEDIA_TYPES, 'status': status, 'updated': UPDATED}] self.requests_mock.get(BASE_URL, json={'versions': version_list}) disc = discover.Discover(self.session, BASE_URL) versions = disc.version_data() self.assertEqual(0, len(versions)) versions = disc.version_data(allow_experimental=True) self.assertEqual(1, len(versions)) self.assertEqual(status, versions[0]['raw_status']) self.assertEqual(V3_URL, versions[0]['url']) self.assertEqual((3, 0), versions[0]['version']) def test_allow_unknown(self): status = 'abcdef' version_list = fixture.DiscoveryList(BASE_URL, v2=False, v3_status=status) self.requests_mock.get(BASE_URL, json=version_list) disc = discover.Discover(self.session, BASE_URL) versions = disc.version_data() self.assertEqual(0, len(versions)) versions = disc.version_data(allow_unknown=True) self.assertEqual(1, len(versions)) self.assertEqual(status, versions[0]['raw_status']) self.assertEqual(V3_URL, versions[0]['url']) self.assertEqual((3, 0), versions[0]['version']) def test_ignoring_invalid_links(self): version_list = [{'id': 'v3.0', 'links': [{'href': V3_URL, 'rel': 'self'}], 'media-types': V3_MEDIA_TYPES, 'status': 'stable', 'updated': UPDATED}, {'id': 'v3.1', 'media-types': V3_MEDIA_TYPES, 'status': 'stable', 'updated': UPDATED}, {'media-types': V3_MEDIA_TYPES, 'status': 'stable', 'updated': UPDATED, 'links': [{'href': V3_URL, 'rel': 'self'}], }] self.requests_mock.get(BASE_URL, json={'versions': version_list}) disc = discover.Discover(self.session, BASE_URL) # raw_version_data will return all choices, even invalid ones versions = disc.raw_version_data() self.assertEqual(3, len(versions)) # only the version with both id and links will be actually returned versions = disc.version_data() self.assertEqual(1, len(versions)) class EndpointDataTests(utils.TestCase): @mock.patch('keystoneauth1.discover.get_discovery') @mock.patch('keystoneauth1.discover.EndpointData.' '_get_discovery_url_choices') def test_run_discovery_cache(self, mock_url_choices, mock_get_disc): # get_discovery raises so we keep looping mock_get_disc.side_effect = exceptions.DiscoveryFailure() # Duplicate 'url1' in here to validate the cache behavior mock_url_choices.return_value = ('url1', 'url2', 'url1', 'url3') epd = discover.EndpointData() epd._run_discovery( session='sess', cache='cache', min_version='min', max_version='max', project_id='projid', allow_version_hack='allow_hack', discover_versions='disc_vers') # Only one call with 'url1' self.assertEqual(3, mock_get_disc.call_count) mock_get_disc.assert_has_calls( [mock.call('sess', url, cache='cache', authenticated=False) for url in ('url1', 'url2', 'url3')]) def test_run_discovery_auth(self): url = 'https://example.com' headers = {'Accept': 'application/json'} session = mock.Mock() session.get.side_effect = [ exceptions.Unauthorized('unauthorized'), # Throw a different exception the second time so that we can # catch it in the test and verify the retry. exceptions.BadRequest('bad request'), ] try: discover.get_version_data(session, url) except exceptions.BadRequest: pass # Only one call with 'url' self.assertEqual(2, session.get.call_count) session.get.assert_has_calls([ mock.call(url, headers=headers, authenticated=None), mock.call(url, headers=headers, authenticated=True), ]) def test_endpoint_data_str(self): """Validate EndpointData.__str__.""" # Populate a few fields to make sure they come through. epd = discover.EndpointData(catalog_url='abc', service_type='123', api_version=(2, 3)) exp = ( 'EndpointData{api_version=(2, 3), catalog_url=abc,' ' endpoint_id=None, interface=None, major_version=None,' ' max_microversion=None, min_microversion=None,' ' next_min_version=None, not_before=None, raw_endpoint=None,' ' region_name=None, service_id=None, service_name=None,' ' service_type=123, service_url=None, url=abc}') # Works with str() self.assertEqual(exp, str(epd)) # Works with implicit stringification self.assertEqual(exp, "%s" % epd) def test_project_id_int_fallback(self): bad_url = "https://compute.example.com/v2/123456" epd = discover.EndpointData(catalog_url=bad_url) self.assertEqual((2, 0), epd.api_version) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/test_fair_sempahore.py0000664000175000017500000000556100000000000025677 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from threading import Thread from timeit import default_timer as timer from unittest import mock from six.moves import queue import testtools from keystoneauth1 import _fair_semaphore class SemaphoreTests(testtools.TestCase): def _thread_worker(self): while True: # get returns the Item, but we don't care about the value so we # purposely don't assign it to anything. self.q.get() with self.s: self.mock_payload.do_something() self.q.task_done() # Have 5 threads do 10 different "things" coordinated by the fair # semaphore. def _concurrency_core(self, concurrency, delay): self.s = _fair_semaphore.FairSemaphore(concurrency, delay) self.q = queue.Queue() for i in range(5): t = Thread(target=self._thread_worker) t.daemon = True t.start() for item in range(0, 10): self.q.put(item) self.q.join() def setUp(self): super(SemaphoreTests, self).setUp() self.mock_payload = mock.Mock() # We should be waiting at least 0.1s between operations, so # the 10 operations must take at *least* 1 second def test_semaphore_no_concurrency(self): start = timer() self._concurrency_core(None, 0.1) end = timer() self.assertTrue((end - start) > 1.0) self.assertEqual(self.mock_payload.do_something.call_count, 10) def test_semaphore_single_concurrency(self): start = timer() self._concurrency_core(1, 0.1) end = timer() self.assertTrue((end - start) > 1.0) self.assertEqual(self.mock_payload.do_something.call_count, 10) def test_semaphore_multiple_concurrency(self): start = timer() self._concurrency_core(5, 0.1) end = timer() self.assertTrue((end - start) > 1.0) self.assertEqual(self.mock_payload.do_something.call_count, 10) # do some high speed tests; I don't think we can really assert # much about these other than they don't deadlock... def test_semaphore_fast_no_concurrency(self): self._concurrency_core(None, 0.0) def test_semaphore_fast_single_concurrency(self): self._concurrency_core(1, 0.0) def test_semaphore_fast_multiple_concurrency(self): self._concurrency_core(5, 0.0) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/test_fixtures.py0000664000175000017500000003256600000000000024571 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from keystoneauth1 import fixture from keystoneauth1.tests.unit import utils class V2TokenTests(utils.TestCase): def test_unscoped(self): token_id = uuid.uuid4().hex user_id = uuid.uuid4().hex user_name = uuid.uuid4().hex token = fixture.V2Token(token_id=token_id, user_id=user_id, user_name=user_name) self.assertEqual(token_id, token.token_id) self.assertEqual(token_id, token['access']['token']['id']) self.assertEqual(user_id, token.user_id) self.assertEqual(user_id, token['access']['user']['id']) self.assertEqual(user_name, token.user_name) self.assertEqual(user_name, token['access']['user']['name']) self.assertIsNone(token.trust_id) def test_tenant_scoped(self): tenant_id = uuid.uuid4().hex tenant_name = uuid.uuid4().hex token = fixture.V2Token(tenant_id=tenant_id, tenant_name=tenant_name) self.assertEqual(tenant_id, token.tenant_id) self.assertEqual(tenant_id, token['access']['token']['tenant']['id']) self.assertEqual(tenant_name, token.tenant_name) tn = token['access']['token']['tenant']['name'] self.assertEqual(tenant_name, tn) self.assertIsNone(token.trust_id) def test_trust_scoped(self): trust_id = uuid.uuid4().hex trustee_user_id = uuid.uuid4().hex token = fixture.V2Token(trust_id=trust_id, trustee_user_id=trustee_user_id) trust = token['access']['trust'] self.assertEqual(trust_id, token.trust_id) self.assertEqual(trust_id, trust['id']) self.assertEqual(trustee_user_id, token.trustee_user_id) self.assertEqual(trustee_user_id, trust['trustee_user_id']) def test_roles(self): role_id1 = uuid.uuid4().hex role_name1 = uuid.uuid4().hex role_id2 = uuid.uuid4().hex role_name2 = uuid.uuid4().hex token = fixture.V2Token() token.add_role(id=role_id1, name=role_name1) token.add_role(id=role_id2, name=role_name2) role_names = token['access']['user']['roles'] role_ids = token['access']['metadata']['roles'] self.assertEqual(set([role_id1, role_id2]), set(role_ids)) for r in (role_name1, role_name2): self.assertIn({'name': r}, role_names) def test_services(self): service_type = uuid.uuid4().hex service_name = uuid.uuid4().hex endpoint_id = uuid.uuid4().hex region = uuid.uuid4().hex public = uuid.uuid4().hex admin = uuid.uuid4().hex internal = uuid.uuid4().hex token = fixture.V2Token() svc = token.add_service(type=service_type, name=service_name) svc.add_endpoint(public=public, admin=admin, internal=internal, region=region, id=endpoint_id) self.assertEqual(1, len(token['access']['serviceCatalog'])) service = token['access']['serviceCatalog'][0]['endpoints'][0] self.assertEqual(public, service['publicURL']) self.assertEqual(internal, service['internalURL']) self.assertEqual(admin, service['adminURL']) self.assertEqual(region, service['region']) self.assertEqual(endpoint_id, service['id']) token.remove_service(type=service_type) self.assertEqual(0, len(token['access']['serviceCatalog'])) def test_token_bind(self): name1 = uuid.uuid4().hex data1 = uuid.uuid4().hex name2 = uuid.uuid4().hex data2 = {uuid.uuid4().hex: uuid.uuid4().hex} token = fixture.V2Token() token.set_bind(name1, data1) token.set_bind(name2, data2) self.assertEqual({name1: data1, name2: data2}, token['access']['token']['bind']) class V3TokenTests(utils.TestCase): def test_unscoped(self): user_id = uuid.uuid4().hex user_name = uuid.uuid4().hex user_domain_id = uuid.uuid4().hex user_domain_name = uuid.uuid4().hex token = fixture.V3Token(user_id=user_id, user_name=user_name, user_domain_id=user_domain_id, user_domain_name=user_domain_name) self.assertEqual(user_id, token.user_id) self.assertEqual(user_id, token['token']['user']['id']) self.assertEqual(user_name, token.user_name) self.assertEqual(user_name, token['token']['user']['name']) user_domain = token['token']['user']['domain'] self.assertEqual(user_domain_id, token.user_domain_id) self.assertEqual(user_domain_id, user_domain['id']) self.assertEqual(user_domain_name, token.user_domain_name) self.assertEqual(user_domain_name, user_domain['name']) def test_project_scoped(self): project_id = uuid.uuid4().hex project_name = uuid.uuid4().hex project_domain_id = uuid.uuid4().hex project_domain_name = uuid.uuid4().hex token = fixture.V3Token(project_id=project_id, project_name=project_name, project_domain_id=project_domain_id, project_domain_name=project_domain_name) self.assertEqual(project_id, token.project_id) self.assertEqual(project_id, token['token']['project']['id']) self.assertEqual(project_name, token.project_name) self.assertEqual(project_name, token['token']['project']['name']) self.assertIsNone(token.get('token', {}).get('is_domain')) project_domain = token['token']['project']['domain'] self.assertEqual(project_domain_id, token.project_domain_id) self.assertEqual(project_domain_id, project_domain['id']) self.assertEqual(project_domain_name, token.project_domain_name) self.assertEqual(project_domain_name, project_domain['name']) def test_project_as_domain_scoped(self): project_id = uuid.uuid4().hex project_name = uuid.uuid4().hex project_domain_id = uuid.uuid4().hex project_domain_name = uuid.uuid4().hex project_is_domain = True token = fixture.V3Token(project_id=project_id, project_name=project_name, project_domain_id=project_domain_id, project_domain_name=project_domain_name, project_is_domain=project_is_domain) self.assertEqual(project_id, token.project_id) self.assertEqual(project_id, token['token']['project']['id']) self.assertEqual(project_name, token.project_name) self.assertEqual(project_name, token['token']['project']['name']) self.assertEqual(project_is_domain, token['token']['is_domain']) project_domain = token['token']['project']['domain'] self.assertEqual(project_domain_id, token.project_domain_id) self.assertEqual(project_domain_id, project_domain['id']) self.assertEqual(project_domain_name, token.project_domain_name) self.assertEqual(project_domain_name, project_domain['name']) def test_domain_scoped(self): domain_id = uuid.uuid4().hex domain_name = uuid.uuid4().hex token = fixture.V3Token(domain_id=domain_id, domain_name=domain_name) self.assertEqual(domain_id, token.domain_id) self.assertEqual(domain_id, token['token']['domain']['id']) self.assertEqual(domain_name, token.domain_name) self.assertEqual(domain_name, token['token']['domain']['name']) def test_roles(self): role1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} role2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} token = fixture.V3Token() token.add_role(**role1) token.add_role(**role2) self.assertEqual(2, len(token['token']['roles'])) self.assertIn(role1, token['token']['roles']) self.assertIn(role2, token['token']['roles']) def test_trust_scoped(self): trust_id = uuid.uuid4().hex trustee_user_id = uuid.uuid4().hex trustor_user_id = uuid.uuid4().hex impersonation = True token = fixture.V3Token(trust_id=trust_id, trustee_user_id=trustee_user_id, trustor_user_id=trustor_user_id, trust_impersonation=impersonation) trust = token['token']['OS-TRUST:trust'] self.assertEqual(trust_id, token.trust_id) self.assertEqual(trust_id, trust['id']) self.assertEqual(trustee_user_id, token.trustee_user_id) self.assertEqual(trustee_user_id, trust['trustee_user']['id']) self.assertEqual(trustor_user_id, token.trustor_user_id) self.assertEqual(trustor_user_id, trust['trustor_user']['id']) self.assertEqual(impersonation, token.trust_impersonation) self.assertEqual(impersonation, trust['impersonation']) def test_oauth_scoped(self): access_id = uuid.uuid4().hex consumer_id = uuid.uuid4().hex token = fixture.V3Token(oauth_access_token_id=access_id, oauth_consumer_id=consumer_id) oauth = token['token']['OS-OAUTH1'] self.assertEqual(access_id, token.oauth_access_token_id) self.assertEqual(access_id, oauth['access_token_id']) self.assertEqual(consumer_id, token.oauth_consumer_id) self.assertEqual(consumer_id, oauth['consumer_id']) def test_catalog(self): service_type = uuid.uuid4().hex service_name = uuid.uuid4().hex service_id = uuid.uuid4().hex region = uuid.uuid4().hex endpoints = {'public': uuid.uuid4().hex, 'internal': uuid.uuid4().hex, 'admin': uuid.uuid4().hex} token = fixture.V3Token() svc = token.add_service(type=service_type, name=service_name, id=service_id) svc.add_standard_endpoints(region=region, **endpoints) self.assertEqual(1, len(token['token']['catalog'])) service = token['token']['catalog'][0] self.assertEqual(3, len(service['endpoints'])) self.assertEqual(service_name, service['name']) self.assertEqual(service_type, service['type']) self.assertEqual(service_id, service['id']) for endpoint in service['endpoints']: # assert an id exists for each endpoint, remove it to make testing # the endpoint content below easier. self.assertTrue(endpoint.pop('id')) for interface, url in endpoints.items(): endpoint = {'interface': interface, 'url': url, 'region': region, 'region_id': region} self.assertIn(endpoint, service['endpoints']) token.remove_service(type=service_type) self.assertEqual(0, len(token['token']['catalog'])) def test_empty_default_service_providers(self): token = fixture.V3Token() self.assertIsNone(token.service_providers) def test_service_providers(self): def new_sp(): return { 'id': uuid.uuid4().hex, 'sp_url': uuid.uuid4().hex, 'auth_url': uuid.uuid4().hex } ref_service_providers = [new_sp(), new_sp()] token = fixture.V3Token() for sp in ref_service_providers: token.add_service_provider(sp['id'], sp['auth_url'], sp['sp_url']) self.assertEqual(ref_service_providers, token.service_providers) self.assertEqual(ref_service_providers, token['token']['service_providers']) def test_token_bind(self): name1 = uuid.uuid4().hex data1 = uuid.uuid4().hex name2 = uuid.uuid4().hex data2 = {uuid.uuid4().hex: uuid.uuid4().hex} token = fixture.V3Token() token.set_bind(name1, data1) token.set_bind(name2, data2) self.assertEqual({name1: data1, name2: data2}, token['token']['bind']) def test_is_admin_project(self): token = fixture.V3Token() self.assertIsNone(token.is_admin_project) self.assertNotIn('is_admin_project', token['token']) token.is_admin_project = True self.assertIs(True, token.is_admin_project) self.assertIs(True, token['token']['is_admin_project']) token.is_admin_project = False self.assertIs(False, token.is_admin_project) self.assertIs(False, token['token']['is_admin_project']) del token.is_admin_project self.assertIsNone(token.is_admin_project) self.assertNotIn('is_admin_project', token['token']) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/test_hacking_checks.py0000664000175000017500000000340100000000000025626 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import textwrap from unittest import mock import pycodestyle import testtools from keystoneauth1.hacking import checks from keystoneauth1.tests.unit import keystoneauth_fixtures class TestCheckOsloNamespaceImports(testtools.TestCase): # We are patching pycodestyle so that only the check under test is actually # installed. @mock.patch('pycodestyle._checks', {'physical_line': {}, 'logical_line': {}, 'tree': {}}) def run_check(self, code): pycodestyle.register_check(checks.check_oslo_namespace_imports) lines = textwrap.dedent(code).strip().splitlines(True) checker = pycodestyle.Checker(lines=lines) checker.check_all() checker.report._deferred_print.sort() return checker.report._deferred_print def assert_has_errors(self, code, expected_errors=None): actual_errors = [e[:3] for e in self.run_check(code)] self.assertEqual(expected_errors or [], actual_errors) def test(self): code_ex = self.useFixture(keystoneauth_fixtures.HackingCode()) code = code_ex.oslo_namespace_imports['code'] errors = code_ex.oslo_namespace_imports['expected_errors'] self.assert_has_errors(code, expected_errors=errors) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/test_http_basic.py0000664000175000017500000000352000000000000025024 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import http_basic from keystoneauth1.loading._plugins import http_basic as loader from keystoneauth1 import session from keystoneauth1.tests.unit import utils class HTTPBasicAuthTest(utils.TestCase): TEST_URL = 'http://server/prefix' def test_basic_case(self): self.requests_mock.get(self.TEST_URL, text='body') a = http_basic.HTTPBasicAuth(username='myName', password='myPassword') s = session.Session(auth=a) data = s.get(self.TEST_URL, authenticated=True) self.assertEqual(data.text, 'body') self.assertRequestHeaderEqual( 'Authorization', 'Basic bXlOYW1lOm15UGFzc3dvcmQ=') self.assertIsNone(a.get_endpoint(s)) def test_basic_options(self): opts = loader.HTTPBasicAuth().get_options() self.assertEqual(['username', 'password', 'endpoint'], [o.name for o in opts]) def test_get_endpoint(self): a = http_basic.HTTPBasicAuth(endpoint=self.TEST_URL) s = session.Session(auth=a) self.assertEqual(self.TEST_URL, a.get_endpoint(s)) def test_get_endpoint_with_override(self): a = http_basic.HTTPBasicAuth(endpoint=self.TEST_URL) s = session.Session(auth=a) self.assertEqual('foo', a.get_endpoint(s, endpoint_override='foo')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/test_matchers.py0000664000175000017500000000651100000000000024515 0ustar00zuulzuul00000000000000# Copyright 2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import testtools from testtools import matchers as tt_matchers from keystoneauth1.tests.unit import matchers as ks_matchers # NOTE(jamielennox): The tests in this file are copied form the non-public # testtools.tests.matchers.helpers.TestMatchersInterface. class TestXMLEquals(testtools.TestCase): matches_xml = b""" """ equivalent_xml = b""" """ mismatches_xml = b""" """ mismatches_description = """expected = actual = """ matches_matcher = ks_matchers.XMLEquals(matches_xml) matches_matches = [matches_xml, equivalent_xml] matches_mismatches = [mismatches_xml] describe_examples = [ (mismatches_description, mismatches_xml, matches_matcher), ] str_examples = [('XMLEquals(%r)' % matches_xml, matches_matcher)] def test_matches_match(self): matcher = self.matches_matcher matches = self.matches_matches mismatches = self.matches_mismatches for candidate in matches: self.assertIsNone(matcher.match(candidate)) for candidate in mismatches: mismatch = matcher.match(candidate) self.assertIsNotNone(mismatch) self.assertIsNotNone(getattr(mismatch, 'describe', None)) def test__str__(self): # [(expected, object to __str__)]. examples = self.str_examples for expected, matcher in examples: self.assertThat(matcher, tt_matchers.DocTestMatches(expected)) def test_describe_difference(self): # [(expected, matchee, matcher), ...] examples = self.describe_examples for difference, matchee, matcher in examples: mismatch = matcher.match(matchee) self.assertEqual(difference, mismatch.describe()) def test_mismatch_details(self): # The mismatch object must provide get_details, which must return a # dictionary mapping names to Content objects. examples = self.describe_examples for difference, matchee, matcher in examples: mismatch = matcher.match(matchee) details = mismatch.get_details() self.assertEqual(dict(details), details) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/test_noauth.py0000664000175000017500000000330100000000000024177 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1.loading._plugins import noauth as loader from keystoneauth1 import noauth from keystoneauth1 import session from keystoneauth1.tests.unit import utils class NoAuthTest(utils.TestCase): NOAUTH_TOKEN = 'notused' TEST_URL = 'http://server/prefix' def test_basic_case(self): self.requests_mock.get(self.TEST_URL, text='body') a = noauth.NoAuth() s = session.Session(auth=a) data = s.get(self.TEST_URL, authenticated=True) self.assertEqual(data.text, 'body') self.assertRequestHeaderEqual('X-Auth-Token', self.NOAUTH_TOKEN) self.assertIsNone(a.get_endpoint(s)) def test_noauth_options(self): opts = loader.NoAuth().get_options() self.assertEqual(['endpoint'], [o.name for o in opts]) def test_get_endpoint(self): a = noauth.NoAuth(endpoint=self.TEST_URL) s = session.Session(auth=a) self.assertEqual(self.TEST_URL, a.get_endpoint(s)) def test_get_endpoint_with_override(self): a = noauth.NoAuth(endpoint=self.TEST_URL) s = session.Session(auth=a) self.assertEqual('foo', a.get_endpoint(s, endpoint_override='foo')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/test_service_token.py0000664000175000017500000001117500000000000025551 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from keystoneauth1 import fixture from keystoneauth1 import identity from keystoneauth1 import service_token from keystoneauth1 import session from keystoneauth1.tests.unit import utils class ServiceTokenTests(utils.TestCase): TEST_URL = 'http://test.example.com/path/' USER_URL = 'http://user-keystone.example.com/v3' SERVICE_URL = 'http://service-keystone.example.com/v3' def setUp(self): super(ServiceTokenTests, self).setUp() self.user_token_id = uuid.uuid4().hex self.user_token = fixture.V3Token() self.user_token.set_project_scope() self.user_auth = identity.V3Password(auth_url=self.USER_URL, user_id=uuid.uuid4().hex, password=uuid.uuid4().hex, project_id=uuid.uuid4().hex) self.service_token_id = uuid.uuid4().hex self.service_token = fixture.V3Token() self.service_token.set_project_scope() self.service_auth = identity.V3Password(auth_url=self.SERVICE_URL, user_id=uuid.uuid4().hex, password=uuid.uuid4().hex, project_id=uuid.uuid4().hex) for t in (self.user_token, self.service_token): s = t.add_service('identity') s.add_standard_endpoints(public='http://keystone.example.com', admin='http://keystone.example.com', internal='http://keystone.example.com') self.test_data = {'data': uuid.uuid4().hex} self.user_mock = self.requests_mock.post( self.USER_URL + '/auth/tokens', json=self.user_token, headers={'X-Subject-Token': self.user_token_id}) self.service_mock = self.requests_mock.post( self.SERVICE_URL + '/auth/tokens', json=self.service_token, headers={'X-Subject-Token': self.service_token_id}) self.requests_mock.get(self.TEST_URL, json=self.test_data) self.combined_auth = service_token.ServiceTokenAuthWrapper( self.user_auth, self.service_auth) self.session = session.Session(auth=self.combined_auth) def test_setting_service_token(self): self.session.get(self.TEST_URL) headers = self.requests_mock.last_request.headers self.assertEqual(self.user_token_id, headers['X-Auth-Token']) self.assertEqual(self.service_token_id, headers['X-Service-Token']) self.assertTrue(self.user_mock.called_once) self.assertTrue(self.service_mock.called_once) def test_invalidation(self): text = uuid.uuid4().hex test_url = 'http://test.example.com/abc' response_list = [{'status_code': 401}, {'text': text}] mock = self.requests_mock.get(test_url, response_list=response_list) resp = self.session.get(test_url) self.assertEqual(text, resp.text) self.assertEqual(2, mock.call_count) self.assertEqual(2, self.user_mock.call_count) self.assertEqual(2, self.service_mock.call_count) def test_pass_throughs(self): self.assertEqual(self.user_auth.get_token(self.session), self.combined_auth.get_token(self.session)) self.assertEqual( self.user_auth.get_endpoint(self.session, 'identity'), self.combined_auth.get_endpoint(self.session, 'identity')) self.assertEqual(self.user_auth.get_user_id(self.session), self.combined_auth.get_user_id(self.session)) self.assertEqual(self.user_auth.get_project_id(self.session), self.combined_auth.get_project_id(self.session)) self.assertEqual(self.user_auth.get_sp_auth_url(self.session, 'a'), self.combined_auth.get_sp_auth_url(self.session, 'a')) self.assertEqual(self.user_auth.get_sp_url(self.session, 'a'), self.combined_auth.get_sp_url(self.session, 'a')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/test_session.py0000664000175000017500000023126600000000000024401 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime import itertools import json import logging import sys from unittest import mock import uuid from oslo_utils import encodeutils import requests import requests.auth import six from testtools import matchers from keystoneauth1 import adapter from keystoneauth1 import discover from keystoneauth1 import exceptions from keystoneauth1 import plugin from keystoneauth1 import session as client_session from keystoneauth1.tests.unit import utils from keystoneauth1 import token_endpoint class RequestsAuth(requests.auth.AuthBase): def __init__(self, *args, **kwargs): super(RequestsAuth, self).__init__(*args, **kwargs) self.header_name = uuid.uuid4().hex self.header_val = uuid.uuid4().hex self.called = False def __call__(self, request): request.headers[self.header_name] = self.header_val self.called = True return request class SessionTests(utils.TestCase): TEST_URL = 'http://127.0.0.1:5000/' def test_get(self): session = client_session.Session() self.stub_url('GET', text='response') resp = session.get(self.TEST_URL) self.assertEqual('GET', self.requests_mock.last_request.method) self.assertEqual(resp.text, 'response') self.assertTrue(resp.ok) def test_post(self): session = client_session.Session() self.stub_url('POST', text='response') resp = session.post(self.TEST_URL, json={'hello': 'world'}) self.assertEqual('POST', self.requests_mock.last_request.method) self.assertEqual(resp.text, 'response') self.assertTrue(resp.ok) self.assertRequestBodyIs(json={'hello': 'world'}) def test_head(self): session = client_session.Session() self.stub_url('HEAD') resp = session.head(self.TEST_URL) self.assertEqual('HEAD', self.requests_mock.last_request.method) self.assertTrue(resp.ok) self.assertRequestBodyIs('') def test_put(self): session = client_session.Session() self.stub_url('PUT', text='response') resp = session.put(self.TEST_URL, json={'hello': 'world'}) self.assertEqual('PUT', self.requests_mock.last_request.method) self.assertEqual(resp.text, 'response') self.assertTrue(resp.ok) self.assertRequestBodyIs(json={'hello': 'world'}) def test_delete(self): session = client_session.Session() self.stub_url('DELETE', text='response') resp = session.delete(self.TEST_URL) self.assertEqual('DELETE', self.requests_mock.last_request.method) self.assertTrue(resp.ok) self.assertEqual(resp.text, 'response') def test_patch(self): session = client_session.Session() self.stub_url('PATCH', text='response') resp = session.patch(self.TEST_URL, json={'hello': 'world'}) self.assertEqual('PATCH', self.requests_mock.last_request.method) self.assertTrue(resp.ok) self.assertEqual(resp.text, 'response') self.assertRequestBodyIs(json={'hello': 'world'}) def test_set_microversion_headers(self): # String microversion, specified service type headers = {} client_session.Session._set_microversion_headers( headers, '2.30', 'compute', None) self.assertEqual(headers['OpenStack-API-Version'], 'compute 2.30') self.assertEqual(headers['X-OpenStack-Nova-API-Version'], '2.30') self.assertEqual(len(headers.keys()), 2) # Tuple microversion, service type via endpoint_filter headers = {} client_session.Session._set_microversion_headers( headers, (2, 30), None, {'service_type': 'compute'}) self.assertEqual(headers['OpenStack-API-Version'], 'compute 2.30') self.assertEqual(headers['X-OpenStack-Nova-API-Version'], '2.30') self.assertEqual(len(headers.keys()), 2) # 'latest' (string) microversion headers = {} client_session.Session._set_microversion_headers( headers, 'latest', 'compute', None) self.assertEqual(headers['OpenStack-API-Version'], 'compute latest') self.assertEqual(headers['X-OpenStack-Nova-API-Version'], 'latest') self.assertEqual(len(headers.keys()), 2) # LATEST (tuple) microversion headers = {} client_session.Session._set_microversion_headers( headers, (discover.LATEST, discover.LATEST), 'compute', None) self.assertEqual(headers['OpenStack-API-Version'], 'compute latest') self.assertEqual(headers['X-OpenStack-Nova-API-Version'], 'latest') self.assertEqual(len(headers.keys()), 2) # ironic microversion, specified service type headers = {} client_session.Session._set_microversion_headers( headers, '2.30', 'baremetal', None) self.assertEqual(headers['OpenStack-API-Version'], 'baremetal 2.30') self.assertEqual(headers['X-OpenStack-Ironic-API-Version'], '2.30') self.assertEqual(len(headers.keys()), 2) # volumev2 service-type - volume microversion headers = {} client_session.Session._set_microversion_headers( headers, (2, 30), None, {'service_type': 'volumev2'}) self.assertEqual(headers['OpenStack-API-Version'], 'volume 2.30') self.assertEqual(len(headers.keys()), 1) # block-storage service-type - volume microversion headers = {} client_session.Session._set_microversion_headers( headers, (2, 30), None, {'service_type': 'block-storage'}) self.assertEqual(headers['OpenStack-API-Version'], 'volume 2.30') self.assertEqual(len(headers.keys()), 1) # shared file system service-type - shared-file-system microversion # (with service type aliases) for service_type in ['sharev2', 'shared-file-system']: headers = {} client_session.Session._set_microversion_headers( headers, (2, 30), None, {'service_type': service_type}) self.assertEqual(headers['X-OpenStack-Manila-API-Version'], '2.30') self.assertEqual(headers['OpenStack-API-Version'], 'shared-file-system 2.30') self.assertEqual(len(headers.keys()), 2) # Headers already exist - no change headers = { 'OpenStack-API-Version': 'compute 2.30', 'X-OpenStack-Nova-API-Version': '2.30', } client_session.Session._set_microversion_headers( headers, (2, 31), None, {'service_type': 'volume'}) self.assertEqual(headers['OpenStack-API-Version'], 'compute 2.30') self.assertEqual(headers['X-OpenStack-Nova-API-Version'], '2.30') # Can't specify a 'M.latest' microversion self.assertRaises(TypeError, client_session.Session._set_microversion_headers, {}, '2.latest', 'service_type', None) self.assertRaises(TypeError, client_session.Session._set_microversion_headers, {}, (2, discover.LATEST), 'service_type', None) # Normalization error self.assertRaises(TypeError, client_session.Session._set_microversion_headers, {}, 'bogus', 'service_type', None) # No service type in param or endpoint filter self.assertRaises(TypeError, client_session.Session._set_microversion_headers, {}, (2, 30), None, None) self.assertRaises(TypeError, client_session.Session._set_microversion_headers, {}, (2, 30), None, {'no_service_type': 'here'}) def test_microversion(self): # microversion not specified session = client_session.Session() self.stub_url('GET', text='response') resp = session.get(self.TEST_URL) self.assertTrue(resp.ok) self.assertRequestNotInHeader('OpenStack-API-Version') session = client_session.Session() self.stub_url('GET', text='response') resp = session.get(self.TEST_URL, microversion='2.30', microversion_service_type='compute', endpoint_filter={'endpoint': 'filter'}) self.assertTrue(resp.ok) self.assertRequestHeaderEqual('OpenStack-API-Version', 'compute 2.30') self.assertRequestHeaderEqual('X-OpenStack-Nova-API-Version', '2.30') def test_user_agent(self): session = client_session.Session() self.stub_url('GET', text='response') resp = session.get(self.TEST_URL) self.assertTrue(resp.ok) self.assertRequestHeaderEqual( 'User-Agent', '%s %s' % ("run.py", client_session.DEFAULT_USER_AGENT)) custom_agent = 'custom-agent/1.0' session = client_session.Session(user_agent=custom_agent) self.stub_url('GET', text='response') resp = session.get(self.TEST_URL) self.assertTrue(resp.ok) self.assertRequestHeaderEqual( 'User-Agent', '%s %s' % (custom_agent, client_session.DEFAULT_USER_AGENT)) resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'}) self.assertTrue(resp.ok) self.assertRequestHeaderEqual('User-Agent', 'new-agent') resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'}, user_agent='overrides-agent') self.assertTrue(resp.ok) self.assertRequestHeaderEqual('User-Agent', 'overrides-agent') # If sys.argv is an empty list, then doesn't fail. with mock.patch.object(sys, 'argv', []): session = client_session.Session() resp = session.get(self.TEST_URL) self.assertTrue(resp.ok) self.assertRequestHeaderEqual( 'User-Agent', client_session.DEFAULT_USER_AGENT) # If sys.argv[0] is an empty string, then doesn't fail. with mock.patch.object(sys, 'argv', ['']): session = client_session.Session() resp = session.get(self.TEST_URL) self.assertTrue(resp.ok) self.assertRequestHeaderEqual( 'User-Agent', client_session.DEFAULT_USER_AGENT) def test_http_session_opts(self): session = client_session.Session(cert='cert.pem', timeout=5, verify='certs') FAKE_RESP = utils.TestResponse({'status_code': 200, 'text': 'resp'}) RESP = mock.Mock(return_value=FAKE_RESP) with mock.patch.object(session.session, 'request', RESP) as mocked: session.post(self.TEST_URL, data='value') mock_args, mock_kwargs = mocked.call_args self.assertEqual(mock_args[0], 'POST') self.assertEqual(mock_args[1], self.TEST_URL) self.assertEqual(mock_kwargs['data'], 'value') self.assertEqual(mock_kwargs['cert'], 'cert.pem') self.assertEqual(mock_kwargs['verify'], 'certs') self.assertEqual(mock_kwargs['timeout'], 5) def test_not_found(self): session = client_session.Session() self.stub_url('GET', status_code=404) self.assertRaises(exceptions.NotFound, session.get, self.TEST_URL) def test_server_error(self): session = client_session.Session() self.stub_url('GET', status_code=500) self.assertRaises(exceptions.InternalServerError, session.get, self.TEST_URL) def test_session_debug_output(self): """Test request and response headers in debug logs. in order to redact secure headers while debug is true. """ session = client_session.Session(verify=False) headers = {'HEADERA': 'HEADERVALB', 'Content-Type': 'application/json'} security_headers = {'Authorization': uuid.uuid4().hex, 'X-Auth-Token': uuid.uuid4().hex, 'X-Subject-Token': uuid.uuid4().hex, 'X-Service-Token': uuid.uuid4().hex} body = '{"a": "b"}' data = '{"c": "d"}' all_headers = dict( itertools.chain(headers.items(), security_headers.items())) self.stub_url('POST', text=body, headers=all_headers) resp = session.post(self.TEST_URL, headers=all_headers, data=data) self.assertEqual(resp.status_code, 200) self.assertIn('curl', self.logger.output) self.assertIn('POST', self.logger.output) self.assertIn('--insecure', self.logger.output) self.assertIn(body, self.logger.output) self.assertIn("'%s'" % data, self.logger.output) for k, v in headers.items(): self.assertIn(k, self.logger.output) self.assertIn(v, self.logger.output) # Assert that response headers contains actual values and # only debug logs has been masked for k, v in security_headers.items(): self.assertIn('%s: {SHA256}' % k, self.logger.output) self.assertEqual(v, resp.headers[k]) self.assertNotIn(v, self.logger.output) def test_session_debug_output_logs_openstack_request_id(self): """Test x-openstack-request-id is logged in debug logs.""" def get_response(log=True): session = client_session.Session(verify=False) endpoint_filter = {'service_name': 'Identity'} headers = {'X-OpenStack-Request-Id': 'req-1234'} body = 'BODYRESPONSE' data = 'BODYDATA' all_headers = dict(itertools.chain(headers.items())) self.stub_url('POST', text=body, headers=all_headers) resp = session.post(self.TEST_URL, endpoint_filter=endpoint_filter, headers=all_headers, data=data, log=log) return resp # if log is disabled then request-id is not logged in debug logs resp = get_response(log=False) self.assertEqual(resp.status_code, 200) expected_log = ('POST call to Identity for %s used request ' 'id req-1234' % self.TEST_URL) self.assertNotIn(expected_log, self.logger.output) # if log is enabled then request-id is logged in debug logs resp = get_response() self.assertEqual(resp.status_code, 200) self.assertIn(expected_log, self.logger.output) def test_logs_failed_output(self): """Test that output is logged even for failed requests.""" session = client_session.Session() body = {uuid.uuid4().hex: uuid.uuid4().hex} self.stub_url('GET', json=body, status_code=400, headers={'Content-Type': 'application/json'}) resp = session.get(self.TEST_URL, raise_exc=False) self.assertEqual(resp.status_code, 400) self.assertIn(list(body.keys())[0], self.logger.output) self.assertIn(list(body.values())[0], self.logger.output) def test_logging_body_only_for_specified_content_types(self): """Verify response body is only logged in specific content types. Response bodies are logged only when the response's Content-Type header is set to application/json. This prevents us to get an unexpected MemoryError when reading arbitrary responses, such as streams. """ OMITTED_BODY = ('Omitted, Content-Type is set to %s. Only ' 'application/json responses have their bodies logged.') session = client_session.Session(verify=False) # Content-Type is not set body = json.dumps({'token': {'id': '...'}}) self.stub_url('POST', text=body) session.post(self.TEST_URL) self.assertNotIn(body, self.logger.output) self.assertIn(OMITTED_BODY % None, self.logger.output) # Content-Type is set to text/xml body = '...' self.stub_url('POST', text=body, headers={'Content-Type': 'text/xml'}) session.post(self.TEST_URL) self.assertNotIn(body, self.logger.output) self.assertIn(OMITTED_BODY % 'text/xml', self.logger.output) # Content-Type is set to application/json body = json.dumps({'token': {'id': '...'}}) self.stub_url('POST', text=body, headers={'Content-Type': 'application/json'}) session.post(self.TEST_URL) self.assertIn(body, self.logger.output) self.assertNotIn(OMITTED_BODY % 'application/json', self.logger.output) # Content-Type is set to application/json; charset=UTF-8 body = json.dumps({'token': {'id': '...'}}) self.stub_url( 'POST', text=body, headers={'Content-Type': 'application/json; charset=UTF-8'}) session.post(self.TEST_URL) self.assertIn(body, self.logger.output) self.assertNotIn(OMITTED_BODY % 'application/json; charset=UTF-8', self.logger.output) def test_logging_cacerts(self): path_to_certs = '/path/to/certs' session = client_session.Session(verify=path_to_certs) self.stub_url('GET', text='text') session.get(self.TEST_URL) self.assertIn('--cacert', self.logger.output) self.assertIn(path_to_certs, self.logger.output) def _connect_retries_check(self, session, expected_retries=0, call_args=None): call_args = call_args or {} self.stub_url('GET', exc=requests.exceptions.Timeout()) call_args['url'] = self.TEST_URL with mock.patch('time.sleep') as m: self.assertRaises(exceptions.ConnectTimeout, session.get, **call_args) self.assertEqual(expected_retries, m.call_count) # 3 retries finishing with 2.0 means 0.5, 1.0 and 2.0 m.assert_called_with(2.0) # we count retries so there will be one initial request + 3 retries self.assertThat(self.requests_mock.request_history, matchers.HasLength(expected_retries + 1)) def test_session_connect_retries(self): retries = 3 session = client_session.Session(connect_retries=retries) self._connect_retries_check(session=session, expected_retries=retries) def test_call_args_connect_retries_session_init(self): session = client_session.Session() retries = 3 call_args = {'connect_retries': retries} self._connect_retries_check(session=session, expected_retries=retries, call_args=call_args) def test_call_args_connect_retries_overrides_session_retries(self): session_retries = 6 call_arg_retries = 3 call_args = {'connect_retries': call_arg_retries} session = client_session.Session(connect_retries=session_retries) self._connect_retries_check(session=session, expected_retries=call_arg_retries, call_args=call_args) def test_override_session_connect_retries_for_request(self): session_retries = 1 session = client_session.Session(connect_retries=session_retries) self.stub_url('GET', exc=requests.exceptions.Timeout()) call_args = {'connect_retries': 0} with mock.patch('time.sleep') as m: self.assertRaises( exceptions.ConnectTimeout, session.request, self.TEST_URL, 'GET', **call_args ) self.assertEqual(0, m.call_count) def test_connect_retries_interval_limit(self): self.stub_url('GET', exc=requests.exceptions.Timeout()) session = client_session.Session() retries = 20 with mock.patch('time.sleep') as m: self.assertRaises(exceptions.ConnectTimeout, session.get, self.TEST_URL, connect_retries=retries) self.assertEqual(retries, m.call_count) # The interval maxes out at 60 m.assert_called_with(60.0) self.assertThat(self.requests_mock.request_history, matchers.HasLength(retries + 1)) def test_connect_retries_fixed_delay(self): self.stub_url('GET', exc=requests.exceptions.Timeout()) session = client_session.Session() retries = 3 with mock.patch('time.sleep') as m: self.assertRaises(exceptions.ConnectTimeout, session.get, self.TEST_URL, connect_retries=retries, connect_retry_delay=0.5) self.assertEqual(retries, m.call_count) m.assert_has_calls([mock.call(0.5)] * retries) # we count retries so there will be one initial request + 3 retries self.assertThat(self.requests_mock.request_history, matchers.HasLength(retries + 1)) def test_http_503_retries(self): self.stub_url('GET', status_code=503) session = client_session.Session() retries = 3 with mock.patch('time.sleep') as m: self.assertRaises(exceptions.ServiceUnavailable, session.get, self.TEST_URL, status_code_retries=retries) self.assertEqual(retries, m.call_count) # 3 retries finishing with 2.0 means 0.5, 1.0 and 2.0 m.assert_called_with(2.0) # we count retries so there will be one initial request + 3 retries self.assertThat(self.requests_mock.request_history, matchers.HasLength(retries + 1)) def test_http_status_retries(self): self.stub_url('GET', status_code=409) session = client_session.Session() retries = 3 with mock.patch('time.sleep') as m: self.assertRaises(exceptions.Conflict, session.get, self.TEST_URL, status_code_retries=retries, retriable_status_codes=[503, 409]) self.assertEqual(retries, m.call_count) # 3 retries finishing with 2.0 means 0.5, 1.0 and 2.0 m.assert_called_with(2.0) # we count retries so there will be one initial request + 3 retries self.assertThat(self.requests_mock.request_history, matchers.HasLength(retries + 1)) def test_http_status_retries_another_code(self): self.stub_url('GET', status_code=404) session = client_session.Session() retries = 3 with mock.patch('time.sleep') as m: self.assertRaises(exceptions.NotFound, session.get, self.TEST_URL, status_code_retries=retries, retriable_status_codes=[503, 409]) self.assertFalse(m.called) self.assertThat(self.requests_mock.request_history, matchers.HasLength(1)) def test_http_status_retries_fixed_delay(self): self.stub_url('GET', status_code=409) session = client_session.Session() retries = 3 with mock.patch('time.sleep') as m: self.assertRaises(exceptions.Conflict, session.get, self.TEST_URL, status_code_retries=retries, status_code_retry_delay=0.5, retriable_status_codes=[503, 409]) self.assertEqual(retries, m.call_count) m.assert_has_calls([mock.call(0.5)] * retries) # we count retries so there will be one initial request + 3 retries self.assertThat(self.requests_mock.request_history, matchers.HasLength(retries + 1)) def test_http_status_retries_inverval_limit(self): self.stub_url('GET', status_code=409) session = client_session.Session() retries = 20 with mock.patch('time.sleep') as m: self.assertRaises(exceptions.Conflict, session.get, self.TEST_URL, status_code_retries=retries, retriable_status_codes=[503, 409]) self.assertEqual(retries, m.call_count) # The interval maxes out at 60 m.assert_called_with(60.0) # we count retries so there will be one initial request + 3 retries self.assertThat(self.requests_mock.request_history, matchers.HasLength(retries + 1)) def test_uses_tcp_keepalive_by_default(self): session = client_session.Session() requests_session = session.session self.assertIsInstance(requests_session.adapters['http://'], client_session.TCPKeepAliveAdapter) self.assertIsInstance(requests_session.adapters['https://'], client_session.TCPKeepAliveAdapter) def test_does_not_set_tcp_keepalive_on_custom_sessions(self): mock_session = mock.Mock() client_session.Session(session=mock_session) self.assertFalse(mock_session.mount.called) def test_ssl_error_message(self): error = uuid.uuid4().hex self.stub_url('GET', exc=requests.exceptions.SSLError(error)) session = client_session.Session() # The exception should contain the URL and details about the SSL error msg = 'SSL exception connecting to %(url)s: %(error)s' % { 'url': self.TEST_URL, 'error': error} self.assertRaisesRegex(exceptions.SSLError, msg, session.get, self.TEST_URL) def test_json_content_type(self): session = client_session.Session() self.stub_url('POST', text='response') resp = session.post( self.TEST_URL, json=[{'op': 'replace', 'path': '/name', 'value': 'new_name'}], headers={'Content-Type': 'application/json-patch+json'}) self.assertEqual('POST', self.requests_mock.last_request.method) self.assertEqual(resp.text, 'response') self.assertTrue(resp.ok) self.assertRequestBodyIs( json=[{'op': 'replace', 'path': '/name', 'value': 'new_name'}]) self.assertContentTypeIs('application/json-patch+json') def test_api_sig_error_message_single(self): title = 'this error is bogus!' detail = 'it is a totally made up error' error_message = { 'errors': [ { 'request_id': uuid.uuid4().hex, 'code': 'phoney.bologna.error', 'status': 500, 'title': title, 'detail': detail, 'links': [ { 'rel': 'help', 'href': 'https://openstack.org' } ] } ] } payload = json.dumps(error_message) self.stub_url('GET', status_code=9000, text=payload, headers={'Content-Type': 'application/json'}) session = client_session.Session() # The exception should contain the information from the error response msg = '{} (HTTP 9000)'.format(title) try: session.get(self.TEST_URL) except exceptions.HttpError as ex: self.assertEqual(ex.message, msg) self.assertEqual(ex.details, detail) def test_api_sig_error_message_multiple(self): title = 'this error is the first error!' detail = 'it is a totally made up error' error_message = { 'errors': [ { 'request_id': uuid.uuid4().hex, 'code': 'phoney.bologna.error', 'status': 500, 'title': title, 'detail': detail, 'links': [ { 'rel': 'help', 'href': 'https://openstack.org' } ] }, { 'request_id': uuid.uuid4().hex, 'code': 'phoney.bologna.error', 'status': 500, 'title': 'some other error', 'detail': detail, 'links': [ { 'rel': 'help', 'href': 'https://openstack.org' } ] } ] } payload = json.dumps(error_message) self.stub_url('GET', status_code=9000, text=payload, headers={'Content-Type': 'application/json'}) session = client_session.Session() # The exception should contain the information from the error response msg = ('Multiple error responses, showing first only: {} (HTTP 9000)' .format(title)) try: session.get(self.TEST_URL) except exceptions.HttpError as ex: self.assertEqual(ex.message, msg) self.assertEqual(ex.details, detail) def test_api_sig_error_message_empty(self): error_message = { 'errors': [ ] } payload = json.dumps(error_message) self.stub_url('GET', status_code=9000, text=payload, headers={'Content-Type': 'application/json'}) session = client_session.Session() # The exception should contain the information from the error response msg = 'HTTP Error (HTTP 9000)' try: session.get(self.TEST_URL) except exceptions.HttpError as ex: self.assertEqual(ex.message, msg) self.assertIsNone(ex.details) def test_error_message_unknown_schema(self): error_message = 'Uh oh, things went bad!' payload = json.dumps(error_message) self.stub_url('GET', status_code=9000, text=payload, headers={'Content-Type': 'application/json'}) session = client_session.Session() msg = 'Unrecognized schema in response body. (HTTP 9000)' try: session.get(self.TEST_URL) except exceptions.HttpError as ex: self.assertEqual(ex.message, msg) class RedirectTests(utils.TestCase): REDIRECT_CHAIN = ['http://myhost:3445/', 'http://anotherhost:6555/', 'http://thirdhost/', 'http://finaldestination:55/'] DEFAULT_REDIRECT_BODY = 'Redirect' DEFAULT_RESP_BODY = 'Found' def setup_redirects(self, method='GET', status_code=305, redirect_kwargs={}, final_kwargs={}): redirect_kwargs.setdefault('text', self.DEFAULT_REDIRECT_BODY) for s, d in zip(self.REDIRECT_CHAIN, self.REDIRECT_CHAIN[1:]): self.requests_mock.register_uri(method, s, status_code=status_code, headers={'Location': d}, **redirect_kwargs) final_kwargs.setdefault('status_code', 200) final_kwargs.setdefault('text', self.DEFAULT_RESP_BODY) self.requests_mock.register_uri(method, self.REDIRECT_CHAIN[-1], **final_kwargs) def assertResponse(self, resp): self.assertEqual(resp.status_code, 200) self.assertEqual(resp.text, self.DEFAULT_RESP_BODY) def test_basic_get(self): session = client_session.Session() self.setup_redirects() resp = session.get(self.REDIRECT_CHAIN[-2]) self.assertResponse(resp) def test_basic_post_keeps_correct_method(self): session = client_session.Session() self.setup_redirects(method='POST', status_code=301) resp = session.post(self.REDIRECT_CHAIN[-2]) self.assertResponse(resp) def test_redirect_forever(self): session = client_session.Session(redirect=True) self.setup_redirects() resp = session.get(self.REDIRECT_CHAIN[0]) self.assertResponse(resp) self.assertTrue(len(resp.history), len(self.REDIRECT_CHAIN)) def test_no_redirect(self): session = client_session.Session(redirect=False) self.setup_redirects() resp = session.get(self.REDIRECT_CHAIN[0]) self.assertEqual(resp.status_code, 305) self.assertEqual(resp.url, self.REDIRECT_CHAIN[0]) def test_redirect_limit(self): self.setup_redirects() for i in (1, 2): session = client_session.Session(redirect=i) resp = session.get(self.REDIRECT_CHAIN[0]) self.assertEqual(resp.status_code, 305) self.assertEqual(resp.url, self.REDIRECT_CHAIN[i]) self.assertEqual(resp.text, self.DEFAULT_REDIRECT_BODY) def test_history_matches_requests(self): self.setup_redirects(status_code=301) session = client_session.Session(redirect=True) req_resp = requests.get(self.REDIRECT_CHAIN[0], allow_redirects=True) ses_resp = session.get(self.REDIRECT_CHAIN[0]) self.assertEqual(len(req_resp.history), len(ses_resp.history)) for r, s in zip(req_resp.history, ses_resp.history): self.assertEqual(r.url, s.url) self.assertEqual(r.status_code, s.status_code) def test_permanent_redirect_308(self): session = client_session.Session() self.setup_redirects(status_code=308) resp = session.get(self.REDIRECT_CHAIN[-2]) self.assertResponse(resp) class AuthPlugin(plugin.BaseAuthPlugin): """Very simple debug authentication plugin. Takes Parameters such that it can throw exceptions at the right times. """ TEST_TOKEN = utils.TestCase.TEST_TOKEN TEST_USER_ID = 'aUser' TEST_PROJECT_ID = 'aProject' SERVICE_URLS = { 'identity': {'public': 'http://identity-public:1111/v2.0', 'admin': 'http://identity-admin:1111/v2.0'}, 'compute': {'public': 'http://compute-public:2222/v1.0', 'admin': 'http://compute-admin:2222/v1.0'}, 'image': {'public': 'http://image-public:3333/v2.0', 'admin': 'http://image-admin:3333/v2.0'} } def __init__(self, token=TEST_TOKEN, invalidate=True): self.token = token self._invalidate = invalidate def get_token(self, session): return self.token def get_endpoint(self, session, service_type=None, interface=None, **kwargs): try: return self.SERVICE_URLS[service_type][interface] except (KeyError, AttributeError): return None def invalidate(self): return self._invalidate def get_user_id(self, session): return self.TEST_USER_ID def get_project_id(self, session): return self.TEST_PROJECT_ID class CalledAuthPlugin(plugin.BaseAuthPlugin): ENDPOINT = 'http://fakeendpoint/' TOKEN = utils.TestCase.TEST_TOKEN USER_ID = uuid.uuid4().hex PROJECT_ID = uuid.uuid4().hex def __init__(self, invalidate=True): self.get_token_called = False self.get_endpoint_called = False self.endpoint_arguments = {} self.invalidate_called = False self.get_project_id_called = False self.get_user_id_called = False self._invalidate = invalidate def get_token(self, session): self.get_token_called = True return self.TOKEN def get_endpoint(self, session, **kwargs): self.get_endpoint_called = True self.endpoint_arguments = kwargs return self.ENDPOINT def invalidate(self): self.invalidate_called = True return self._invalidate def get_project_id(self, session, **kwargs): self.get_project_id_called = True return self.PROJECT_ID def get_user_id(self, session, **kwargs): self.get_user_id_called = True return self.USER_ID class SessionAuthTests(utils.TestCase): TEST_URL = 'http://127.0.0.1:5000/' TEST_JSON = {'hello': 'world'} def stub_service_url(self, service_type, interface, path, method='GET', **kwargs): base_url = AuthPlugin.SERVICE_URLS[service_type][interface] uri = "%s/%s" % (base_url.rstrip('/'), path.lstrip('/')) self.requests_mock.register_uri(method, uri, **kwargs) def test_auth_plugin_default_with_plugin(self): self.stub_url('GET', base_url=self.TEST_URL, json=self.TEST_JSON) # if there is an auth_plugin then it should default to authenticated auth = AuthPlugin() sess = client_session.Session(auth=auth) resp = sess.get(self.TEST_URL) self.assertEqual(resp.json(), self.TEST_JSON) self.assertRequestHeaderEqual('X-Auth-Token', AuthPlugin.TEST_TOKEN) def test_auth_plugin_disable(self): self.stub_url('GET', base_url=self.TEST_URL, json=self.TEST_JSON) auth = AuthPlugin() sess = client_session.Session(auth=auth) resp = sess.get(self.TEST_URL, authenticated=False) self.assertEqual(resp.json(), self.TEST_JSON) self.assertRequestHeaderEqual('X-Auth-Token', None) def test_object_delete(self): auth = AuthPlugin() sess = client_session.Session(auth=auth) mock_close = mock.Mock() sess._session.close = mock_close del sess self.assertEqual(1, mock_close.call_count) def test_service_type_urls(self): service_type = 'compute' interface = 'public' path = '/instances' status = 200 body = 'SUCCESS' self.stub_service_url(service_type=service_type, interface=interface, path=path, status_code=status, text=body) sess = client_session.Session(auth=AuthPlugin()) resp = sess.get(path, endpoint_filter={'service_type': service_type, 'interface': interface}) self.assertEqual(self.requests_mock.last_request.url, AuthPlugin.SERVICE_URLS['compute']['public'] + path) self.assertEqual(resp.text, body) self.assertEqual(resp.status_code, status) def test_service_url_raises_if_no_auth_plugin(self): sess = client_session.Session() self.assertRaises(exceptions.MissingAuthPlugin, sess.get, '/path', endpoint_filter={'service_type': 'compute', 'interface': 'public'}) def test_service_url_raises_if_no_url_returned(self): sess = client_session.Session(auth=AuthPlugin()) self.assertRaises(exceptions.EndpointNotFound, sess.get, '/path', endpoint_filter={'service_type': 'unknown', 'interface': 'public'}) def test_raises_exc_only_when_asked(self): # A request that returns a HTTP error should by default raise an # exception by default, if you specify raise_exc=False then it will not self.requests_mock.get(self.TEST_URL, status_code=401) sess = client_session.Session() self.assertRaises(exceptions.Unauthorized, sess.get, self.TEST_URL) resp = sess.get(self.TEST_URL, raise_exc=False) self.assertEqual(401, resp.status_code) def test_passed_auth_plugin(self): passed = CalledAuthPlugin() sess = client_session.Session() self.requests_mock.get(CalledAuthPlugin.ENDPOINT + 'path', status_code=200) endpoint_filter = {'service_type': 'identity'} # no plugin with authenticated won't work self.assertRaises(exceptions.MissingAuthPlugin, sess.get, 'path', authenticated=True) # no plugin with an endpoint filter won't work self.assertRaises(exceptions.MissingAuthPlugin, sess.get, 'path', authenticated=False, endpoint_filter=endpoint_filter) resp = sess.get('path', auth=passed, endpoint_filter=endpoint_filter) self.assertEqual(200, resp.status_code) self.assertTrue(passed.get_endpoint_called) self.assertTrue(passed.get_token_called) def test_passed_auth_plugin_overrides(self): fixed = CalledAuthPlugin() passed = CalledAuthPlugin() sess = client_session.Session(fixed) self.requests_mock.get(CalledAuthPlugin.ENDPOINT + 'path', status_code=200) resp = sess.get('path', auth=passed, endpoint_filter={'service_type': 'identity'}) self.assertEqual(200, resp.status_code) self.assertTrue(passed.get_endpoint_called) self.assertTrue(passed.get_token_called) self.assertFalse(fixed.get_endpoint_called) self.assertFalse(fixed.get_token_called) def test_requests_auth_plugin(self): sess = client_session.Session() requests_auth = RequestsAuth() self.requests_mock.get(self.TEST_URL, text='resp') sess.get(self.TEST_URL, requests_auth=requests_auth) last = self.requests_mock.last_request self.assertEqual(requests_auth.header_val, last.headers[requests_auth.header_name]) self.assertTrue(requests_auth.called) def test_reauth_called(self): auth = CalledAuthPlugin(invalidate=True) sess = client_session.Session(auth=auth) self.requests_mock.get(self.TEST_URL, [{'text': 'Failed', 'status_code': 401}, {'text': 'Hello', 'status_code': 200}]) # allow_reauth=True is the default resp = sess.get(self.TEST_URL, authenticated=True) self.assertEqual(200, resp.status_code) self.assertEqual('Hello', resp.text) self.assertTrue(auth.invalidate_called) def test_reauth_not_called(self): auth = CalledAuthPlugin(invalidate=True) sess = client_session.Session(auth=auth) self.requests_mock.get(self.TEST_URL, [{'text': 'Failed', 'status_code': 401}, {'text': 'Hello', 'status_code': 200}]) self.assertRaises(exceptions.Unauthorized, sess.get, self.TEST_URL, authenticated=True, allow_reauth=False) self.assertFalse(auth.invalidate_called) def test_endpoint_override_overrides_filter(self): auth = CalledAuthPlugin() sess = client_session.Session(auth=auth) override_base = 'http://mytest/' path = 'path' override_url = override_base + path resp_text = uuid.uuid4().hex self.requests_mock.get(override_url, text=resp_text) resp = sess.get(path, endpoint_override=override_base, endpoint_filter={'service_type': 'identity'}) self.assertEqual(resp_text, resp.text) self.assertEqual(override_url, self.requests_mock.last_request.url) self.assertTrue(auth.get_token_called) self.assertFalse(auth.get_endpoint_called) self.assertFalse(auth.get_user_id_called) self.assertFalse(auth.get_project_id_called) def test_endpoint_override_ignore_full_url(self): auth = CalledAuthPlugin() sess = client_session.Session(auth=auth) path = 'path' url = self.TEST_URL + path resp_text = uuid.uuid4().hex self.requests_mock.get(url, text=resp_text) resp = sess.get(url, endpoint_override='http://someother.url', endpoint_filter={'service_type': 'identity'}) self.assertEqual(resp_text, resp.text) self.assertEqual(url, self.requests_mock.last_request.url) self.assertTrue(auth.get_token_called) self.assertFalse(auth.get_endpoint_called) self.assertFalse(auth.get_user_id_called) self.assertFalse(auth.get_project_id_called) def test_endpoint_override_does_id_replacement(self): auth = CalledAuthPlugin() sess = client_session.Session(auth=auth) override_base = 'http://mytest/%(project_id)s/%(user_id)s' path = 'path' replacements = {'user_id': CalledAuthPlugin.USER_ID, 'project_id': CalledAuthPlugin.PROJECT_ID} override_url = override_base % replacements + '/' + path resp_text = uuid.uuid4().hex self.requests_mock.get(override_url, text=resp_text) resp = sess.get(path, endpoint_override=override_base, endpoint_filter={'service_type': 'identity'}) self.assertEqual(resp_text, resp.text) self.assertEqual(override_url, self.requests_mock.last_request.url) self.assertTrue(auth.get_token_called) self.assertTrue(auth.get_user_id_called) self.assertTrue(auth.get_project_id_called) self.assertFalse(auth.get_endpoint_called) def test_endpoint_override_fails_to_replace_if_none(self): # The token_endpoint plugin doesn't know user_id or project_id auth = token_endpoint.Token(uuid.uuid4().hex, uuid.uuid4().hex) sess = client_session.Session(auth=auth) override_base = 'http://mytest/%(project_id)s' e = self.assertRaises(ValueError, sess.get, '/path', endpoint_override=override_base, endpoint_filter={'service_type': 'identity'}) self.assertIn('project_id', str(e)) override_base = 'http://mytest/%(user_id)s' e = self.assertRaises(ValueError, sess.get, '/path', endpoint_override=override_base, endpoint_filter={'service_type': 'identity'}) self.assertIn('user_id', str(e)) def test_endpoint_override_fails_to_do_unknown_replacement(self): auth = CalledAuthPlugin() sess = client_session.Session(auth=auth) override_base = 'http://mytest/%(unknown_id)s' e = self.assertRaises(AttributeError, sess.get, '/path', endpoint_override=override_base, endpoint_filter={'service_type': 'identity'}) self.assertIn('unknown_id', str(e)) def test_user_and_project_id(self): auth = AuthPlugin() sess = client_session.Session(auth=auth) self.assertEqual(auth.TEST_USER_ID, sess.get_user_id()) self.assertEqual(auth.TEST_PROJECT_ID, sess.get_project_id()) def test_logger_object_passed(self): logger = logging.getLogger(uuid.uuid4().hex) logger.setLevel(logging.DEBUG) logger.propagate = False io = six.StringIO() handler = logging.StreamHandler(io) logger.addHandler(handler) auth = AuthPlugin() sess = client_session.Session(auth=auth) response = {uuid.uuid4().hex: uuid.uuid4().hex} self.stub_url('GET', json=response, headers={'Content-Type': 'application/json'}) resp = sess.get(self.TEST_URL, logger=logger) self.assertEqual(response, resp.json()) output = io.getvalue() self.assertIn(self.TEST_URL, output) self.assertIn(list(response.keys())[0], output) self.assertIn(list(response.values())[0], output) self.assertNotIn(list(response.keys())[0], self.logger.output) self.assertNotIn(list(response.values())[0], self.logger.output) def test_split_loggers(self): def get_logger_io(name): logger_name = 'keystoneauth.session.{name}'.format(name=name) logger = logging.getLogger(logger_name) logger.setLevel(logging.DEBUG) io = six.StringIO() handler = logging.StreamHandler(io) logger.addHandler(handler) return io io = {} for name in ('request', 'body', 'response', 'request-id'): io[name] = get_logger_io(name) auth = AuthPlugin() sess = client_session.Session(auth=auth, split_loggers=True) response_key = uuid.uuid4().hex response_val = uuid.uuid4().hex response = {response_key: response_val} request_id = uuid.uuid4().hex self.stub_url( 'GET', json=response, headers={ 'Content-Type': 'application/json', 'X-OpenStack-Request-ID': request_id, }) resp = sess.get( self.TEST_URL, headers={ encodeutils.safe_encode('x-bytes-header'): encodeutils.safe_encode('bytes-value') }) self.assertEqual(response, resp.json()) request_output = io['request'].getvalue().strip() response_output = io['response'].getvalue().strip() body_output = io['body'].getvalue().strip() id_output = io['request-id'].getvalue().strip() self.assertIn('curl -g -i -X GET {url}'.format(url=self.TEST_URL), request_output) self.assertIn('-H "x-bytes-header: bytes-value"', request_output) self.assertEqual('[200] Content-Type: application/json ' 'X-OpenStack-Request-ID: ' '{id}'.format(id=request_id), response_output) self.assertEqual( 'GET call to {url} used request id {id}'.format( url=self.TEST_URL, id=request_id), id_output) self.assertEqual( '{{"{key}": "{val}"}}'.format( key=response_key, val=response_val), body_output) def test_collect_timing(self): auth = AuthPlugin() sess = client_session.Session(auth=auth, collect_timing=True) response = {uuid.uuid4().hex: uuid.uuid4().hex} self.stub_url('GET', json=response, headers={'Content-Type': 'application/json'}) resp = sess.get(self.TEST_URL) self.assertEqual(response, resp.json()) timings = sess.get_timings() self.assertEqual(timings[0].method, 'GET') self.assertEqual(timings[0].url, self.TEST_URL) self.assertIsInstance(timings[0].elapsed, datetime.timedelta) sess.reset_timings() timings = sess.get_timings() self.assertEqual(len(timings), 0) class AdapterTest(utils.TestCase): SERVICE_TYPE = uuid.uuid4().hex SERVICE_NAME = uuid.uuid4().hex INTERFACE = uuid.uuid4().hex REGION_NAME = uuid.uuid4().hex USER_AGENT = uuid.uuid4().hex VERSION = uuid.uuid4().hex ALLOW = {'allow_deprecated': False, 'allow_experimental': True, 'allow_unknown': True} TEST_URL = CalledAuthPlugin.ENDPOINT def _create_loaded_adapter(self, sess=None, auth=None): return adapter.Adapter(sess or client_session.Session(), auth=auth or CalledAuthPlugin(), service_type=self.SERVICE_TYPE, service_name=self.SERVICE_NAME, interface=self.INTERFACE, region_name=self.REGION_NAME, user_agent=self.USER_AGENT, version=self.VERSION, allow=self.ALLOW) def _verify_endpoint_called(self, adpt): self.assertEqual(self.SERVICE_TYPE, adpt.auth.endpoint_arguments['service_type']) self.assertEqual(self.SERVICE_NAME, adpt.auth.endpoint_arguments['service_name']) self.assertEqual(self.INTERFACE, adpt.auth.endpoint_arguments['interface']) self.assertEqual(self.REGION_NAME, adpt.auth.endpoint_arguments['region_name']) self.assertEqual(self.VERSION, adpt.auth.endpoint_arguments['version']) def test_setting_variables_on_request(self): response = uuid.uuid4().hex self.stub_url('GET', text=response) adpt = self._create_loaded_adapter() resp = adpt.get('/') self.assertEqual(resp.text, response) self._verify_endpoint_called(adpt) self.assertEqual(self.ALLOW, adpt.auth.endpoint_arguments['allow']) self.assertTrue(adpt.auth.get_token_called) self.assertRequestHeaderEqual('User-Agent', self.USER_AGENT) def test_setting_global_id_on_request(self): global_id_adpt = "req-%s" % uuid.uuid4() global_id_req = "req-%s" % uuid.uuid4() response = uuid.uuid4().hex self.stub_url('GET', text=response) def mk_adpt(**kwargs): return adapter.Adapter(client_session.Session(), auth=CalledAuthPlugin(), service_type=self.SERVICE_TYPE, service_name=self.SERVICE_NAME, interface=self.INTERFACE, region_name=self.REGION_NAME, user_agent=self.USER_AGENT, version=self.VERSION, allow=self.ALLOW, **kwargs) # No global_request_id adpt = mk_adpt() resp = adpt.get('/') self.assertEqual(resp.text, response) self._verify_endpoint_called(adpt) self.assertEqual(self.ALLOW, adpt.auth.endpoint_arguments['allow']) self.assertTrue(adpt.auth.get_token_called) self.assertRequestHeaderEqual('X-OpenStack-Request-ID', None) # global_request_id only on the request adpt.get('/', global_request_id=global_id_req) self.assertRequestHeaderEqual('X-OpenStack-Request-ID', global_id_req) # global_request_id only on the adapter adpt = mk_adpt(global_request_id=global_id_adpt) adpt.get('/') self.assertRequestHeaderEqual('X-OpenStack-Request-ID', global_id_adpt) # global_request_id on the adapter *and* the request (the request takes # precedence) adpt.get('/', global_request_id=global_id_req) self.assertRequestHeaderEqual('X-OpenStack-Request-ID', global_id_req) def test_setting_variables_on_get_endpoint(self): adpt = self._create_loaded_adapter() url = adpt.get_endpoint() self.assertEqual(self.TEST_URL, url) self._verify_endpoint_called(adpt) def test_legacy_binding(self): key = uuid.uuid4().hex val = uuid.uuid4().hex response = json.dumps({key: val}) self.stub_url('GET', text=response) auth = CalledAuthPlugin() sess = client_session.Session(auth=auth) adpt = adapter.LegacyJsonAdapter(sess, service_type=self.SERVICE_TYPE, user_agent=self.USER_AGENT) resp, body = adpt.get('/') self.assertEqual(self.SERVICE_TYPE, auth.endpoint_arguments['service_type']) self.assertEqual(resp.text, response) self.assertEqual(val, body[key]) def test_legacy_binding_non_json_resp(self): response = uuid.uuid4().hex self.stub_url('GET', text=response, headers={'Content-Type': 'text/html'}) auth = CalledAuthPlugin() sess = client_session.Session(auth=auth) adpt = adapter.LegacyJsonAdapter(sess, service_type=self.SERVICE_TYPE, user_agent=self.USER_AGENT) resp, body = adpt.get('/') self.assertEqual(self.SERVICE_TYPE, auth.endpoint_arguments['service_type']) self.assertEqual(resp.text, response) self.assertIsNone(body) def test_methods(self): sess = client_session.Session() adpt = adapter.Adapter(sess) url = 'http://url' for method in ['get', 'head', 'post', 'put', 'patch', 'delete']: with mock.patch.object(adpt, 'request') as m: getattr(adpt, method)(url) m.assert_called_once_with(url, method.upper()) def test_setting_endpoint_override(self): endpoint_override = 'http://overrideurl' path = '/path' endpoint_url = endpoint_override + path auth = CalledAuthPlugin() sess = client_session.Session(auth=auth) adpt = adapter.Adapter(sess, endpoint_override=endpoint_override) response = uuid.uuid4().hex self.requests_mock.get(endpoint_url, text=response) resp = adpt.get(path) self.assertEqual(response, resp.text) self.assertEqual(endpoint_url, self.requests_mock.last_request.url) self.assertEqual(endpoint_override, adpt.get_endpoint()) def test_adapter_invalidate(self): auth = CalledAuthPlugin() sess = client_session.Session() adpt = adapter.Adapter(sess, auth=auth) adpt.invalidate() self.assertTrue(auth.invalidate_called) def test_adapter_get_token(self): auth = CalledAuthPlugin() sess = client_session.Session() adpt = adapter.Adapter(sess, auth=auth) self.assertEqual(self.TEST_TOKEN, adpt.get_token()) self.assertTrue(auth.get_token_called) def test_adapter_connect_retries(self): retries = 2 sess = client_session.Session() adpt = adapter.Adapter(sess, connect_retries=retries) self.stub_url('GET', exc=requests.exceptions.ConnectionError()) with mock.patch('time.sleep') as m: self.assertRaises(exceptions.ConnectionError, adpt.get, self.TEST_URL) self.assertEqual(retries, m.call_count) # we count retries so there will be one initial request + 2 retries self.assertThat(self.requests_mock.request_history, matchers.HasLength(retries + 1)) def test_adapter_http_503_retries(self): retries = 2 sess = client_session.Session() adpt = adapter.Adapter(sess, status_code_retries=retries) self.stub_url('GET', status_code=503) with mock.patch('time.sleep') as m: self.assertRaises(exceptions.ServiceUnavailable, adpt.get, self.TEST_URL) self.assertEqual(retries, m.call_count) # we count retries so there will be one initial request + 2 retries self.assertThat(self.requests_mock.request_history, matchers.HasLength(retries + 1)) def test_adapter_http_status_retries(self): retries = 2 sess = client_session.Session() adpt = adapter.Adapter(sess, status_code_retries=retries, retriable_status_codes=[503, 409]) self.stub_url('GET', status_code=409) with mock.patch('time.sleep') as m: self.assertRaises(exceptions.Conflict, adpt.get, self.TEST_URL) self.assertEqual(retries, m.call_count) # we count retries so there will be one initial request + 2 retries self.assertThat(self.requests_mock.request_history, matchers.HasLength(retries + 1)) def test_user_and_project_id(self): auth = AuthPlugin() sess = client_session.Session() adpt = adapter.Adapter(sess, auth=auth) self.assertEqual(auth.TEST_USER_ID, adpt.get_user_id()) self.assertEqual(auth.TEST_PROJECT_ID, adpt.get_project_id()) def test_logger_object_passed(self): logger = logging.getLogger(uuid.uuid4().hex) logger.setLevel(logging.DEBUG) logger.propagate = False io = six.StringIO() handler = logging.StreamHandler(io) logger.addHandler(handler) auth = AuthPlugin() sess = client_session.Session(auth=auth) adpt = adapter.Adapter(sess, auth=auth, logger=logger) response = {uuid.uuid4().hex: uuid.uuid4().hex} self.stub_url('GET', json=response, headers={'Content-Type': 'application/json'}) resp = adpt.get(self.TEST_URL, logger=logger) self.assertEqual(response, resp.json()) output = io.getvalue() self.assertIn(self.TEST_URL, output) self.assertIn(list(response.keys())[0], output) self.assertIn(list(response.values())[0], output) self.assertNotIn(list(response.keys())[0], self.logger.output) self.assertNotIn(list(response.values())[0], self.logger.output) def test_unknown_connection_error(self): self.stub_url('GET', exc=requests.exceptions.RequestException) self.assertRaises(exceptions.UnknownConnectionError, client_session.Session().request, self.TEST_URL, 'GET') def test_additional_headers(self): session_key = uuid.uuid4().hex session_val = uuid.uuid4().hex adapter_key = uuid.uuid4().hex adapter_val = uuid.uuid4().hex request_key = uuid.uuid4().hex request_val = uuid.uuid4().hex text = uuid.uuid4().hex url = 'http://keystone.test.com' self.requests_mock.get(url, text=text) sess = client_session.Session( additional_headers={session_key: session_val}) adap = adapter.Adapter(session=sess, additional_headers={adapter_key: adapter_val}) resp = adap.get(url, headers={request_key: request_val}) request = self.requests_mock.last_request self.assertEqual(resp.text, text) self.assertEqual(session_val, request.headers[session_key]) self.assertEqual(adapter_val, request.headers[adapter_key]) self.assertEqual(request_val, request.headers[request_key]) def test_additional_headers_overrides(self): header = uuid.uuid4().hex session_val = uuid.uuid4().hex adapter_val = uuid.uuid4().hex request_val = uuid.uuid4().hex url = 'http://keystone.test.com' self.requests_mock.get(url) sess = client_session.Session(additional_headers={header: session_val}) adap = adapter.Adapter(session=sess) adap.get(url) self.assertEqual(session_val, self.requests_mock.last_request.headers[header]) adap.additional_headers[header] = adapter_val adap.get(url) self.assertEqual(adapter_val, self.requests_mock.last_request.headers[header]) adap.get(url, headers={header: request_val}) self.assertEqual(request_val, self.requests_mock.last_request.headers[header]) def test_adapter_user_agent_session_adapter(self): sess = client_session.Session(app_name='ksatest', app_version='1.2.3') adap = adapter.Adapter(client_name='testclient', client_version='4.5.6', session=sess) url = 'http://keystone.test.com' self.requests_mock.get(url) adap.get(url) agent = 'ksatest/1.2.3 testclient/4.5.6' self.assertEqual(agent + ' ' + client_session.DEFAULT_USER_AGENT, self.requests_mock.last_request.headers['User-Agent']) def test_adapter_user_agent_session_version_on_adapter(self): class TestAdapter(adapter.Adapter): client_name = 'testclient' client_version = '4.5.6' sess = client_session.Session(app_name='ksatest', app_version='1.2.3') adap = TestAdapter(session=sess) url = 'http://keystone.test.com' self.requests_mock.get(url) adap.get(url) agent = 'ksatest/1.2.3 testclient/4.5.6' self.assertEqual(agent + ' ' + client_session.DEFAULT_USER_AGENT, self.requests_mock.last_request.headers['User-Agent']) def test_adapter_user_agent_session_adapter_no_app_version(self): sess = client_session.Session(app_name='ksatest') adap = adapter.Adapter(client_name='testclient', client_version='4.5.6', session=sess) url = 'http://keystone.test.com' self.requests_mock.get(url) adap.get(url) agent = 'ksatest testclient/4.5.6' self.assertEqual(agent + ' ' + client_session.DEFAULT_USER_AGENT, self.requests_mock.last_request.headers['User-Agent']) def test_adapter_user_agent_session_adapter_no_client_version(self): sess = client_session.Session(app_name='ksatest', app_version='1.2.3') adap = adapter.Adapter(client_name='testclient', session=sess) url = 'http://keystone.test.com' self.requests_mock.get(url) adap.get(url) agent = 'ksatest/1.2.3 testclient' self.assertEqual(agent + ' ' + client_session.DEFAULT_USER_AGENT, self.requests_mock.last_request.headers['User-Agent']) def test_adapter_user_agent_session_adapter_additional(self): sess = client_session.Session(app_name='ksatest', app_version='1.2.3', additional_user_agent=[('one', '1.1.1'), ('two', '2.2.2')]) adap = adapter.Adapter(client_name='testclient', client_version='4.5.6', session=sess) url = 'http://keystone.test.com' self.requests_mock.get(url) adap.get(url) agent = 'ksatest/1.2.3 testclient/4.5.6 one/1.1.1 two/2.2.2' self.assertEqual(agent + ' ' + client_session.DEFAULT_USER_AGENT, self.requests_mock.last_request.headers['User-Agent']) def test_adapter_user_agent_session(self): sess = client_session.Session(app_name='ksatest', app_version='1.2.3') adap = adapter.Adapter(session=sess) url = 'http://keystone.test.com' self.requests_mock.get(url) adap.get(url) agent = 'ksatest/1.2.3' self.assertEqual(agent + ' ' + client_session.DEFAULT_USER_AGENT, self.requests_mock.last_request.headers['User-Agent']) def test_adapter_user_agent_adapter(self): sess = client_session.Session() adap = adapter.Adapter(client_name='testclient', client_version='4.5.6', session=sess) url = 'http://keystone.test.com' self.requests_mock.get(url) adap.get(url) agent = 'testclient/4.5.6' self.assertEqual(agent + ' ' + client_session.DEFAULT_USER_AGENT, self.requests_mock.last_request.headers['User-Agent']) def test_adapter_user_agent_session_override(self): sess = client_session.Session(app_name='ksatest', app_version='1.2.3', additional_user_agent=[('one', '1.1.1'), ('two', '2.2.2')]) adap = adapter.Adapter(client_name='testclient', client_version='4.5.6', session=sess) url = 'http://keystone.test.com' self.requests_mock.get(url) override_user_agent = '%s/%s' % (uuid.uuid4().hex, uuid.uuid4().hex) adap.get(url, user_agent=override_user_agent) self.assertEqual(override_user_agent, self.requests_mock.last_request.headers['User-Agent']) def test_nested_adapters(self): text = uuid.uuid4().hex token = uuid.uuid4().hex url = 'http://keystone.example.com/path' sess = client_session.Session() auth = CalledAuthPlugin() auth.ENDPOINT = url auth.TOKEN = token adap1 = adapter.Adapter(session=sess, interface='public') adap2 = adapter.Adapter(session=adap1, service_type='identity', auth=auth) self.requests_mock.get(url + '/test', text=text) resp = adap2.get('/test') self.assertEqual(text, resp.text) self.assertTrue(auth.get_endpoint_called) self.assertEqual('public', auth.endpoint_arguments['interface']) self.assertEqual('identity', auth.endpoint_arguments['service_type']) last_token = self.requests_mock.last_request.headers['X-Auth-Token'] self.assertEqual(token, last_token) def test_default_microversion(self): sess = client_session.Session() url = 'http://url' def validate(adap_kwargs, get_kwargs, exp_kwargs): with mock.patch.object(sess, 'request') as m: adapter.Adapter(sess, **adap_kwargs).get(url, **get_kwargs) m.assert_called_once_with(url, 'GET', endpoint_filter={}, headers={}, rate_semaphore=mock.ANY, **exp_kwargs) # No default_microversion in Adapter, no microversion in get() validate({}, {}, {}) # default_microversion in Adapter, no microversion in get() validate({'default_microversion': '1.2'}, {}, {'microversion': '1.2'}) # No default_microversion in Adapter, microversion specified in get() validate({}, {'microversion': '1.2'}, {'microversion': '1.2'}) # microversion in get() overrides default_microversion in Adapter validate({'default_microversion': '1.2'}, {'microversion': '1.5'}, {'microversion': '1.5'}) def test_raise_exc_override(self): sess = client_session.Session() url = 'http://url' def validate(adap_kwargs, get_kwargs, exp_kwargs): with mock.patch.object(sess, 'request') as m: adapter.Adapter(sess, **adap_kwargs).get(url, **get_kwargs) m.assert_called_once_with(url, 'GET', endpoint_filter={}, headers={}, rate_semaphore=mock.ANY, **exp_kwargs) # No raise_exc in Adapter or get() validate({}, {}, {}) # Set in Adapter, unset in get() validate({'raise_exc': True}, {}, {'raise_exc': True}) validate({'raise_exc': False}, {}, {'raise_exc': False}) # Unset in Adapter, set in get() validate({}, {'raise_exc': True}, {'raise_exc': True}) validate({}, {'raise_exc': False}, {'raise_exc': False}) # Setting in get() overrides the one in Adapter validate({'raise_exc': True}, {'raise_exc': False}, {'raise_exc': False}) validate({'raise_exc': False}, {'raise_exc': True}, {'raise_exc': True}) class TCPKeepAliveAdapterTest(utils.TestCase): def setUp(self): super(TCPKeepAliveAdapterTest, self).setUp() self.init_poolmanager = self.patch( client_session.requests.adapters.HTTPAdapter, 'init_poolmanager') self.constructor = self.patch( client_session.TCPKeepAliveAdapter, '__init__', lambda self: None) def test_init_poolmanager_with_requests_lesser_than_2_4_1(self): self.patch(client_session, 'REQUESTS_VERSION', (2, 4, 0)) given_adapter = client_session.TCPKeepAliveAdapter() # when pool manager is initialized given_adapter.init_poolmanager(1, 2, 3) # then no socket_options are given self.init_poolmanager.assert_called_once_with(1, 2, 3) def test_init_poolmanager_with_basic_options(self): self.patch(client_session, 'REQUESTS_VERSION', (2, 4, 1)) socket = self.patch_socket_with_options( ['IPPROTO_TCP', 'TCP_NODELAY', 'SOL_SOCKET', 'SO_KEEPALIVE']) given_adapter = client_session.TCPKeepAliveAdapter() # when pool manager is initialized given_adapter.init_poolmanager(1, 2, 3) # then no socket_options are given self.init_poolmanager.assert_called_once_with( 1, 2, 3, socket_options=[ (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)]) def test_init_poolmanager_with_tcp_keepidle(self): self.patch(client_session, 'REQUESTS_VERSION', (2, 4, 1)) socket = self.patch_socket_with_options( ['IPPROTO_TCP', 'TCP_NODELAY', 'SOL_SOCKET', 'SO_KEEPALIVE', 'TCP_KEEPIDLE']) given_adapter = client_session.TCPKeepAliveAdapter() # when pool manager is initialized given_adapter.init_poolmanager(1, 2, 3) # then socket_options are given self.init_poolmanager.assert_called_once_with( 1, 2, 3, socket_options=[ (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)]) def test_init_poolmanager_with_tcp_keepcnt(self): self.patch(client_session, 'REQUESTS_VERSION', (2, 4, 1)) self.patch(client_session.utils, 'is_windows_linux_subsystem', False) socket = self.patch_socket_with_options( ['IPPROTO_TCP', 'TCP_NODELAY', 'SOL_SOCKET', 'SO_KEEPALIVE', 'TCP_KEEPCNT']) given_adapter = client_session.TCPKeepAliveAdapter() # when pool manager is initialized given_adapter.init_poolmanager(1, 2, 3) # then socket_options are given self.init_poolmanager.assert_called_once_with( 1, 2, 3, socket_options=[ (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 4)]) def test_init_poolmanager_with_tcp_keepcnt_on_windows(self): self.patch(client_session, 'REQUESTS_VERSION', (2, 4, 1)) self.patch(client_session.utils, 'is_windows_linux_subsystem', True) socket = self.patch_socket_with_options( ['IPPROTO_TCP', 'TCP_NODELAY', 'SOL_SOCKET', 'SO_KEEPALIVE', 'TCP_KEEPCNT']) given_adapter = client_session.TCPKeepAliveAdapter() # when pool manager is initialized given_adapter.init_poolmanager(1, 2, 3) # then socket_options are given self.init_poolmanager.assert_called_once_with( 1, 2, 3, socket_options=[ (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)]) def test_init_poolmanager_with_tcp_keepintvl(self): self.patch(client_session, 'REQUESTS_VERSION', (2, 4, 1)) socket = self.patch_socket_with_options( ['IPPROTO_TCP', 'TCP_NODELAY', 'SOL_SOCKET', 'SO_KEEPALIVE', 'TCP_KEEPINTVL']) given_adapter = client_session.TCPKeepAliveAdapter() # when pool manager is initialized given_adapter.init_poolmanager(1, 2, 3) # then socket_options are given self.init_poolmanager.assert_called_once_with( 1, 2, 3, socket_options=[ (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 15)]) def test_init_poolmanager_with_given_optionsl(self): self.patch(client_session, 'REQUESTS_VERSION', (2, 4, 1)) given_adapter = client_session.TCPKeepAliveAdapter() given_options = object() # when pool manager is initialized given_adapter.init_poolmanager(1, 2, 3, socket_options=given_options) # then socket_options are given self.init_poolmanager.assert_called_once_with( 1, 2, 3, socket_options=given_options) def patch_socket_with_options(self, option_names): # to mock socket module with exactly the attributes I want I create # a class with that attributes socket = type('socket', (object,), {name: 'socket.' + name for name in option_names}) return self.patch(client_session, 'socket', socket) def patch(self, target, name, *args, **kwargs): context = mock.patch.object(target, name, *args, **kwargs) patch = context.start() self.addCleanup(context.stop) return patch ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/test_token_endpoint.py0000664000175000017500000000511100000000000025722 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from testtools import matchers from keystoneauth1.loading._plugins import admin_token as loader from keystoneauth1 import session from keystoneauth1.tests.unit import utils from keystoneauth1 import token_endpoint class TokenEndpointTest(utils.TestCase): TEST_TOKEN = 'aToken' TEST_URL = 'http://server/prefix' def test_basic_case(self): self.requests_mock.get(self.TEST_URL, text='body') a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN) s = session.Session(auth=a) data = s.get(self.TEST_URL, authenticated=True) self.assertEqual(data.text, 'body') self.assertRequestHeaderEqual('X-Auth-Token', self.TEST_TOKEN) def test_basic_endpoint_case(self): self.stub_url('GET', ['p'], text='body') a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN) s = session.Session(auth=a) data = s.get('/p', authenticated=True, endpoint_filter={'service': 'identity'}) self.assertEqual(self.TEST_URL, a.get_endpoint(s)) self.assertEqual('body', data.text) self.assertRequestHeaderEqual('X-Auth-Token', self.TEST_TOKEN) def test_token_endpoint_user_id(self): a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN) s = session.Session() # we can't know this information about this sort of plugin self.assertIsNone(a.get_user_id(s)) self.assertIsNone(a.get_project_id(s)) class AdminTokenTest(utils.TestCase): def test_token_endpoint_options(self): opt_names = [opt.name for opt in loader.AdminToken().get_options()] self.assertThat(opt_names, matchers.HasLength(2)) self.assertIn('token', opt_names) self.assertIn('endpoint', opt_names) def test_token_endpoint_deprecated_options(self): endpoint_opt = [ opt for opt in loader.AdminToken().get_options() if opt.name == 'endpoint'][0] opt_names = [opt.name for opt in endpoint_opt.deprecated] self.assertEqual(['url'], opt_names) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/test_utils.py0000664000175000017500000000142700000000000024050 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import testtools from keystoneauth1 import _utils class UtilsTests(testtools.TestCase): def test_get_logger(self): self.assertEqual('keystoneauth.tests.unit.test_utils', _utils.get_logger(__name__).name) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/tests/unit/utils.py0000664000175000017500000001264400000000000023014 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json as jsonutils import logging import time import uuid import fixtures import requests from requests_mock.contrib import fixture from six.moves.urllib import parse as urlparse import testtools class TestCase(testtools.TestCase): TEST_DOMAIN_ID = uuid.uuid4().hex TEST_DOMAIN_NAME = uuid.uuid4().hex TEST_GROUP_ID = uuid.uuid4().hex TEST_ROLE_ID = uuid.uuid4().hex TEST_TENANT_ID = uuid.uuid4().hex TEST_TENANT_NAME = uuid.uuid4().hex TEST_RECEIPT = uuid.uuid4().hex TEST_TOKEN = uuid.uuid4().hex TEST_TRUST_ID = uuid.uuid4().hex TEST_USER = uuid.uuid4().hex TEST_USER_ID = uuid.uuid4().hex TEST_ROOT_URL = 'http://127.0.0.1:5000/' def setUp(self): super(TestCase, self).setUp() self.logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) fixtures.MockPatchObject(time, 'time', lambda: 1234) self.requests_mock = self.useFixture(fixture.Fixture()) def stub_url(self, method, parts=None, base_url=None, json=None, **kwargs): if not base_url: base_url = self.TEST_URL if json: kwargs['text'] = jsonutils.dumps(json) headers = kwargs.setdefault('headers', {}) headers.setdefault('Content-Type', 'application/json') if parts: url = '/'.join([p.strip('/') for p in [base_url] + parts]) else: url = base_url url = url.replace("/?", "?") return self.requests_mock.register_uri(method, url, **kwargs) def assertRequestBodyIs(self, body=None, json=None): last_request_body = self.requests_mock.last_request.body if json: val = jsonutils.loads(last_request_body) self.assertEqual(json, val) elif body: self.assertEqual(body, last_request_body) def assertContentTypeIs(self, content_type): last_request = self.requests_mock.last_request self.assertEqual(last_request.headers['Content-Type'], content_type) def assertQueryStringIs(self, qs=''): r"""Verify the QueryString matches what is expected. The qs parameter should be of the format \'foo=bar&abc=xyz\' """ expected = urlparse.parse_qs(qs, keep_blank_values=True) parts = urlparse.urlparse(self.requests_mock.last_request.url) querystring = urlparse.parse_qs(parts.query, keep_blank_values=True) self.assertEqual(expected, querystring) def assertQueryStringContains(self, **kwargs): """Verify the query string contains the expected parameters. This method is used to verify that the query string for the most recent request made contains all the parameters provided as ``kwargs``, and that the value of each parameter contains the value for the kwarg. If the value for the kwarg is an empty string (''), then all that's verified is that the parameter is present. """ parts = urlparse.urlparse(self.requests_mock.last_request.url) qs = urlparse.parse_qs(parts.query, keep_blank_values=True) for k, v in kwargs.items(): self.assertIn(k, qs) self.assertIn(v, qs[k]) def assertRequestHeaderEqual(self, name, val): """Verify that the last request made contains a header and its value. The request must have already been made. """ headers = self.requests_mock.last_request.headers self.assertEqual(headers.get(name), val) def assertRequestNotInHeader(self, name): """Verify that the last request made does not contain a header key. The request must have already been made. """ headers = self.requests_mock.last_request.headers self.assertNotIn(name, headers) class TestResponse(requests.Response): """Class used to wrap requests.Response. This provides some convenience to initialize with a dict. """ def __init__(self, data): self._text = None super(TestResponse, self).__init__() if isinstance(data, dict): self.status_code = data.get('status_code', 200) headers = data.get('headers') if headers: self.headers.update(headers) # Fake the text attribute to streamline Response creation # _content is defined by requests.Response self._content = data.get('text') else: self.status_code = data def __eq__(self, other): """Define equiality behavior of request and response.""" return self.__dict__ == other.__dict__ # NOTE: This function is only needed by Python 2. If we get to point where # we don't support Python 2 anymore, this function should be removed. def __ne__(self, other): """Define inequiality behavior of request and response.""" return not self.__eq__(other) @property def text(self): return self.content ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/keystoneauth1/token_endpoint.py0000664000175000017500000000623600000000000022553 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import plugin class Token(plugin.BaseAuthPlugin): """A provider that will always use the given token and endpoint. This is really only useful for testing and in certain CLI cases where you have a known endpoint and admin token that you want to use. """ def __init__(self, endpoint, token): super(Token, self).__init__() # NOTE(jamielennox): endpoint is reserved for when plugins # can be used to provide that information self.endpoint = endpoint self.token = token def get_token(self, session): return self.token def get_endpoint_data(self, session, endpoint_override=None, discover_versions=True, **kwargs): """Return a valid endpoint data for a the service. :param session: A session object that can be used for communication. :type session: keystoneauth1.session.Session :param str endpoint_override: URL to use for version discovery other than the endpoint stored in the plugin. (optional, defaults to None) :param bool discover_versions: Whether to get version metadata from the version discovery document even if it major api version info can be inferred from the url. (optional, defaults to True) :param kwargs: Ignored. :raises keystoneauth1.exceptions.http.HttpError: An error from an invalid HTTP response. :return: Valid EndpointData or None if not available. :rtype: `keystoneauth1.discover.EndpointData` or None """ return super(Token, self).get_endpoint_data( session, endpoint_override=endpoint_override or self.endpoint, discover_versions=discover_versions, **kwargs) def get_endpoint(self, session, **kwargs): """Return the supplied endpoint. Using this plugin the same endpoint is returned regardless of the parameters passed to the plugin. """ return self.endpoint def get_auth_ref(self, session, **kwargs): """Return the authentication reference of an auth plugin. :param session: A session object to be used for communication :type session: keystoneauth1.session.session """ # token plugin does not have an auth ref, because it's a # "static" authentication using a pre-existing token. return None ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2767942 keystoneauth1-4.4.0/keystoneauth1.egg-info/0000775000175000017500000000000000000000000020644 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897232.0 keystoneauth1-4.4.0/keystoneauth1.egg-info/PKG-INFO0000664000175000017500000000475700000000000021756 0ustar00zuulzuul00000000000000Metadata-Version: 2.1 Name: keystoneauth1 Version: 4.4.0 Summary: Authentication Library for OpenStack Identity Home-page: https://docs.openstack.org/keystoneauth/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/keystoneauth.svg :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on ============ keystoneauth ============ .. image:: https://img.shields.io/pypi/v/keystoneauth1.svg :target:https://pypi.org/project/keystoneauth1 :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/keystoneauth1.svg :target: https://pypi.org/project/keystoneauth1/ :alt: Downloads This package contains tools for authenticating to an OpenStack-based cloud. These tools include: * Authentication plugins (password, token, and federation based) * Discovery mechanisms to determine API version support * A session that is used to maintain client settings across requests (based on the requests Python library) Further information: * Free software: Apache license * Documentation: https://docs.openstack.org/keystoneauth/latest/ * Source: https://opendev.org/openstack/keystoneauth * Bugs: https://bugs.launchpad.net/keystoneauth * Release notes: https://docs.openstack.org/releasenotes/keystoneauth/ Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Requires-Python: >=3.6 Provides-Extra: betamax Provides-Extra: kerberos Provides-Extra: oauth1 Provides-Extra: saml2 Provides-Extra: test ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897232.0 keystoneauth1-4.4.0/keystoneauth1.egg-info/SOURCES.txt0000664000175000017500000002644300000000000022541 0ustar00zuulzuul00000000000000.coveragerc .mailmap .stestr.conf .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE README.rst bindep.txt lower-constraints.txt requirements.txt setup.cfg setup.py test-requirements.txt tox.ini doc/.gitignore doc/Makefile doc/requirements.txt doc/ext/__init__.py doc/ext/list_plugins.py doc/source/authentication-plugins.rst doc/source/conf.py doc/source/extras.rst doc/source/index.rst doc/source/migrating.rst doc/source/plugin-options.rst doc/source/using-sessions.rst doc/source/images/graphs_authComp.svg doc/source/images/graphs_authCompDelegate.svg keystoneauth1/__init__.py keystoneauth1/_fair_semaphore.py keystoneauth1/_utils.py keystoneauth1/adapter.py keystoneauth1/discover.py keystoneauth1/http_basic.py keystoneauth1/noauth.py keystoneauth1/plugin.py keystoneauth1/service_token.py keystoneauth1/session.py keystoneauth1/token_endpoint.py keystoneauth1.egg-info/PKG-INFO keystoneauth1.egg-info/SOURCES.txt keystoneauth1.egg-info/dependency_links.txt keystoneauth1.egg-info/entry_points.txt keystoneauth1.egg-info/not-zip-safe keystoneauth1.egg-info/pbr.json keystoneauth1.egg-info/requires.txt keystoneauth1.egg-info/top_level.txt keystoneauth1/access/__init__.py keystoneauth1/access/access.py keystoneauth1/access/service_catalog.py keystoneauth1/access/service_providers.py keystoneauth1/exceptions/__init__.py keystoneauth1/exceptions/auth.py keystoneauth1/exceptions/auth_plugins.py keystoneauth1/exceptions/base.py keystoneauth1/exceptions/catalog.py keystoneauth1/exceptions/connection.py keystoneauth1/exceptions/discovery.py keystoneauth1/exceptions/http.py keystoneauth1/exceptions/oidc.py keystoneauth1/exceptions/response.py keystoneauth1/exceptions/service_providers.py keystoneauth1/extras/__init__.py keystoneauth1/extras/_saml2/__init__.py keystoneauth1/extras/_saml2/_loading.py keystoneauth1/extras/_saml2/v3/__init__.py keystoneauth1/extras/_saml2/v3/adfs.py keystoneauth1/extras/_saml2/v3/base.py keystoneauth1/extras/_saml2/v3/saml2.py keystoneauth1/extras/kerberos/__init__.py keystoneauth1/extras/kerberos/_loading.py keystoneauth1/extras/oauth1/__init__.py keystoneauth1/extras/oauth1/_loading.py keystoneauth1/extras/oauth1/v3.py keystoneauth1/fixture/__init__.py keystoneauth1/fixture/discovery.py keystoneauth1/fixture/exception.py keystoneauth1/fixture/hooks.py keystoneauth1/fixture/keystoneauth_betamax.py keystoneauth1/fixture/plugin.py keystoneauth1/fixture/serializer.py keystoneauth1/fixture/v2.py keystoneauth1/fixture/v3.py keystoneauth1/hacking/__init__.py keystoneauth1/hacking/checks.py keystoneauth1/identity/__init__.py keystoneauth1/identity/access.py keystoneauth1/identity/base.py keystoneauth1/identity/v2.py keystoneauth1/identity/generic/__init__.py keystoneauth1/identity/generic/base.py keystoneauth1/identity/generic/password.py keystoneauth1/identity/generic/token.py keystoneauth1/identity/v3/__init__.py keystoneauth1/identity/v3/application_credential.py keystoneauth1/identity/v3/base.py keystoneauth1/identity/v3/federation.py keystoneauth1/identity/v3/k2k.py keystoneauth1/identity/v3/multi_factor.py keystoneauth1/identity/v3/oidc.py keystoneauth1/identity/v3/password.py keystoneauth1/identity/v3/receipt.py keystoneauth1/identity/v3/token.py keystoneauth1/identity/v3/tokenless_auth.py keystoneauth1/identity/v3/totp.py keystoneauth1/loading/__init__.py keystoneauth1/loading/_utils.py keystoneauth1/loading/adapter.py keystoneauth1/loading/base.py keystoneauth1/loading/cli.py keystoneauth1/loading/conf.py keystoneauth1/loading/identity.py keystoneauth1/loading/opts.py keystoneauth1/loading/session.py keystoneauth1/loading/_plugins/__init__.py keystoneauth1/loading/_plugins/admin_token.py keystoneauth1/loading/_plugins/http_basic.py keystoneauth1/loading/_plugins/noauth.py keystoneauth1/loading/_plugins/identity/__init__.py keystoneauth1/loading/_plugins/identity/generic.py keystoneauth1/loading/_plugins/identity/v2.py keystoneauth1/loading/_plugins/identity/v3.py keystoneauth1/tests/__init__.py keystoneauth1/tests/unit/__init__.py keystoneauth1/tests/unit/client_fixtures.py keystoneauth1/tests/unit/k2k_fixtures.py keystoneauth1/tests/unit/keystoneauth_fixtures.py keystoneauth1/tests/unit/matchers.py keystoneauth1/tests/unit/oidc_fixtures.py keystoneauth1/tests/unit/test_betamax_fixture.py keystoneauth1/tests/unit/test_betamax_hooks.py keystoneauth1/tests/unit/test_betamax_serializer.py keystoneauth1/tests/unit/test_discovery.py keystoneauth1/tests/unit/test_fair_sempahore.py keystoneauth1/tests/unit/test_fixtures.py keystoneauth1/tests/unit/test_hacking_checks.py keystoneauth1/tests/unit/test_http_basic.py keystoneauth1/tests/unit/test_matchers.py keystoneauth1/tests/unit/test_noauth.py keystoneauth1/tests/unit/test_service_token.py keystoneauth1/tests/unit/test_session.py keystoneauth1/tests/unit/test_token_endpoint.py keystoneauth1/tests/unit/test_utils.py keystoneauth1/tests/unit/utils.py keystoneauth1/tests/unit/access/__init__.py keystoneauth1/tests/unit/access/test_v2_access.py keystoneauth1/tests/unit/access/test_v2_service_catalog.py keystoneauth1/tests/unit/access/test_v3_access.py keystoneauth1/tests/unit/access/test_v3_service_catalog.py keystoneauth1/tests/unit/data/README keystoneauth1/tests/unit/data/keystone_v2_sample_request.json keystoneauth1/tests/unit/data/keystone_v2_sample_response.json keystoneauth1/tests/unit/data/keystone_v3_sample_request.json keystoneauth1/tests/unit/data/keystone_v3_sample_response.json keystoneauth1/tests/unit/data/ksa_betamax_test_cassette.yaml keystoneauth1/tests/unit/data/ksa_serializer_data.json keystoneauth1/tests/unit/data/test_pre_record_hook.json keystoneauth1/tests/unit/exceptions/__init__.py keystoneauth1/tests/unit/exceptions/test_exceptions.py keystoneauth1/tests/unit/extras/__init__.py keystoneauth1/tests/unit/extras/kerberos/__init__.py keystoneauth1/tests/unit/extras/kerberos/base.py keystoneauth1/tests/unit/extras/kerberos/test_fedkerb_loading.py keystoneauth1/tests/unit/extras/kerberos/test_kerberos_loading.py keystoneauth1/tests/unit/extras/kerberos/test_mapped.py keystoneauth1/tests/unit/extras/kerberos/test_v3.py keystoneauth1/tests/unit/extras/kerberos/utils.py keystoneauth1/tests/unit/extras/oauth1/__init__.py keystoneauth1/tests/unit/extras/oauth1/test_oauth1.py keystoneauth1/tests/unit/extras/oauth1/test_oauth1_loading.py keystoneauth1/tests/unit/extras/saml2/__init__.py keystoneauth1/tests/unit/extras/saml2/test_auth_adfs.py keystoneauth1/tests/unit/extras/saml2/test_auth_saml2.py keystoneauth1/tests/unit/extras/saml2/utils.py keystoneauth1/tests/unit/extras/saml2/examples/xml/ADFS_RequestSecurityTokenResponse.xml keystoneauth1/tests/unit/extras/saml2/examples/xml/ADFS_fault.xml keystoneauth1/tests/unit/extras/saml2/fixtures/__init__.py keystoneauth1/tests/unit/extras/saml2/fixtures/templates/authn_request.xml keystoneauth1/tests/unit/extras/saml2/fixtures/templates/saml_assertion.xml keystoneauth1/tests/unit/extras/saml2/fixtures/templates/soap_response.xml keystoneauth1/tests/unit/identity/__init__.py keystoneauth1/tests/unit/identity/test_access.py keystoneauth1/tests/unit/identity/test_identity_common.py keystoneauth1/tests/unit/identity/test_identity_v2.py keystoneauth1/tests/unit/identity/test_identity_v3.py keystoneauth1/tests/unit/identity/test_identity_v3_federation.py keystoneauth1/tests/unit/identity/test_identity_v3_oidc.py keystoneauth1/tests/unit/identity/test_password.py keystoneauth1/tests/unit/identity/test_token.py keystoneauth1/tests/unit/identity/test_tokenless_auth.py keystoneauth1/tests/unit/identity/utils.py keystoneauth1/tests/unit/loading/__init__.py keystoneauth1/tests/unit/loading/test_adapter.py keystoneauth1/tests/unit/loading/test_cli.py keystoneauth1/tests/unit/loading/test_conf.py keystoneauth1/tests/unit/loading/test_entry_points.py keystoneauth1/tests/unit/loading/test_fixtures.py keystoneauth1/tests/unit/loading/test_generic.py keystoneauth1/tests/unit/loading/test_loading.py keystoneauth1/tests/unit/loading/test_session.py keystoneauth1/tests/unit/loading/test_v3.py keystoneauth1/tests/unit/loading/utils.py releasenotes/notes/.placeholder releasenotes/notes/1583780-700f99713e06324e.yaml releasenotes/notes/add-oidc-client-credentials-2be065926ba4b849.yaml releasenotes/notes/add-oidc-discovery-document-support-b07fe54f83286d62.yaml releasenotes/notes/add-prompt-to-opt-d083acc357a7f07b.yaml releasenotes/notes/add-totp-auth-plugin-0650d220899c25b7.yaml releasenotes/notes/additional-headers-f2d16f85f5abe942.yaml releasenotes/notes/allow_version_hack-flag-9b53b72d9b084c04.yaml releasenotes/notes/api-sig-error-guideline-handler.yaml releasenotes/notes/basic-http-auth-45bea4298209df75.yaml releasenotes/notes/bp-application-credentials-416a1f8bb2311e04.yaml releasenotes/notes/bp-system-scope-29e9c597039ddb1e.yaml releasenotes/notes/bug-1582774-49af731b6dfc6f2f.yaml releasenotes/notes/bug-1614688-c4a1bd54f4ba5644.yaml releasenotes/notes/bug-1616105-cc8b85eb056e99e2.yaml releasenotes/notes/bug-1654847-acdf9543158329ec.yaml releasenotes/notes/bug-1689424-set-adfspassword-endpointreference-f186d84a54007b09.yaml releasenotes/notes/bug-1733052-1b4af3b3fe1b05bb.yaml releasenotes/notes/bug-1766235wq-0de60d0f996c6bfb.yaml releasenotes/notes/bug-1839748-5d8dfc99c43aaefc.yaml releasenotes/notes/bug-1840235-ef2946d149ac329c.yaml releasenotes/notes/bug-1876317-1db97d1b12a3e4b4.yaml releasenotes/notes/cache-trailing-slash-3663c86cd9754379.yaml releasenotes/notes/cleanup-session-on-delete-1ed6177d4c5c1f83.yaml releasenotes/notes/client-side-rate-limiting-dec43fc9b54f5b70.yaml releasenotes/notes/collect-timing-85f007f0d86c8b26.yaml releasenotes/notes/drop-py-2-7-f90c67a5db0dfeb8.yaml releasenotes/notes/drop-python-3.5-362bb9d47f830353.yaml releasenotes/notes/expose-endpoint-status-6195a6b76d8a8de8.yaml releasenotes/notes/filter-versions-service-type-763af68092344b7a.yaml releasenotes/notes/fix-get-all-version-data-a01ee58524755b9b.yaml releasenotes/notes/get-auth-ref-7418e13bd0942060.yaml releasenotes/notes/global_request_id-per-request-bd66c7e0f1a71d9f.yaml releasenotes/notes/improve-http-error-handling.yaml releasenotes/notes/ironic-discovery-fe41793ef97027bf.yaml releasenotes/notes/ironic-microversions-a69bf92ab21f0cf5.yaml releasenotes/notes/ksa_2.2.0-81145229d4b43043.yaml releasenotes/notes/microversion-header-support-901acd820a21d788.yaml releasenotes/notes/noauth-discovery-c26d82a32c36d41d.yaml releasenotes/notes/none-auth-dab13ab9af6f5c86.yaml releasenotes/notes/oslo-config-split-loggers-6bda266d657fe921.yaml releasenotes/notes/retries-limit-dbaedcb3207934ae.yaml releasenotes/notes/retries-options-99e4dbc240941557.yaml releasenotes/notes/retry-authenticated-discovery-19c4354ff983f507.yaml releasenotes/notes/retry-delay-68d0c0a1dffcf2fd.yaml releasenotes/notes/serice-type-aliases-249454829c57f39a.yaml releasenotes/notes/status-code-retries-75052a43efa4edb2.yaml releasenotes/notes/support-api-wg-discovery-2cb4b0186619e124.yaml releasenotes/notes/user-agent-generation-b069100508c06177.yaml releasenotes/notes/version-between-b4b0bcf4cecfb9e4.yaml releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/mitaka.rst releasenotes/source/newton.rst releasenotes/source/ocata.rst releasenotes/source/pike.rst releasenotes/source/queens.rst releasenotes/source/rocky.rst releasenotes/source/stein.rst releasenotes/source/train.rst releasenotes/source/unreleased.rst releasenotes/source/ussuri.rst releasenotes/source/victoria.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897232.0 keystoneauth1-4.4.0/keystoneauth1.egg-info/dependency_links.txt0000664000175000017500000000000100000000000024712 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897232.0 keystoneauth1-4.4.0/keystoneauth1.egg-info/entry_points.txt0000664000175000017500000000303500000000000024143 0ustar00zuulzuul00000000000000[keystoneauth1.plugin] admin_token = keystoneauth1.loading._plugins.admin_token:AdminToken http_basic = keystoneauth1.loading._plugins.http_basic:HTTPBasicAuth none = keystoneauth1.loading._plugins.noauth:NoAuth password = keystoneauth1.loading._plugins.identity.generic:Password token = keystoneauth1.loading._plugins.identity.generic:Token v2password = keystoneauth1.loading._plugins.identity.v2:Password v2token = keystoneauth1.loading._plugins.identity.v2:Token v3adfspassword = keystoneauth1.extras._saml2._loading:ADFSPassword v3applicationcredential = keystoneauth1.loading._plugins.identity.v3:ApplicationCredential v3fedkerb = keystoneauth1.extras.kerberos._loading:MappedKerberos v3kerberos = keystoneauth1.extras.kerberos._loading:Kerberos v3multifactor = keystoneauth1.loading._plugins.identity.v3:MultiFactor v3oauth1 = keystoneauth1.extras.oauth1._loading:V3OAuth1 v3oidcaccesstoken = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectAccessToken v3oidcauthcode = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectAuthorizationCode v3oidcclientcredentials = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectClientCredentials v3oidcpassword = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectPassword v3password = keystoneauth1.loading._plugins.identity.v3:Password v3samlpassword = keystoneauth1.extras._saml2._loading:Saml2Password v3token = keystoneauth1.loading._plugins.identity.v3:Token v3tokenlessauth = keystoneauth1.loading._plugins.identity.v3:TokenlessAuth v3totp = keystoneauth1.loading._plugins.identity.v3:TOTP ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897232.0 keystoneauth1-4.4.0/keystoneauth1.egg-info/not-zip-safe0000664000175000017500000000000100000000000023072 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897232.0 keystoneauth1-4.4.0/keystoneauth1.egg-info/pbr.json0000664000175000017500000000005600000000000022323 0ustar00zuulzuul00000000000000{"git_version": "112bcae", "is_release": true}././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897232.0 keystoneauth1-4.4.0/keystoneauth1.egg-info/requires.txt0000664000175000017500000000117400000000000023247 0ustar00zuulzuul00000000000000iso8601>=0.1.11 os-service-types>=1.2.0 pbr!=2.1.0,>=2.0.0 requests>=2.14.2 six>=1.10.0 stevedore>=1.20.0 [betamax] betamax>=0.7.0 fixtures>=3.0.0 mock>=2.0.0 [kerberos] requests-kerberos>=0.8.0 [oauth1] oauthlib>=0.6.2 [saml2] lxml>=4.2.0 [test] PyYAML>=3.12 bandit<1.6.0,>=1.1.0 betamax>=0.7.0 coverage!=4.4,>=4.0 fixtures>=3.0.0 flake8-docstrings==0.2.1.post1 flake8-import-order>=0.17.1 hacking<3.1.0,>=3.0.1 lxml>=4.2.0 oauthlib>=0.6.2 oslo.config>=5.2.0 oslo.utils>=3.33.0 oslotest>=3.2.0 pycodestyle<2.6.0,>=2.0.0 reno>=3.1.0 requests-kerberos>=0.8.0 requests-mock>=1.2.0 stestr>=1.0.0 testresources>=2.0.0 testtools>=2.2.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897232.0 keystoneauth1-4.4.0/keystoneauth1.egg-info/top_level.txt0000664000175000017500000000001600000000000023373 0ustar00zuulzuul00000000000000keystoneauth1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/lower-constraints.txt0000664000175000017500000000161500000000000020607 0ustar00zuulzuul00000000000000appdirs==1.4.3 Babel==2.8.0 bandit==1.1.0 betamax==0.7.0 certifi==2018.1.18 coverage==4.0 debtcollector==1.19.0 dulwich==0.19.0 entrypoints==0.3 extras==1.0.0 fixtures==3.0.0 future==0.16.0 gitdb2==2.0.3 GitPython==2.1.8 iso8601==0.1.11 linecache2==1.0.0 lxml==4.2.0 mock==2.0.0 monotonic==1.4 mox3==0.25.0 netaddr==0.7.19 netifaces==0.10.6 oauthlib==0.6.2 os-client-config==1.29.0 os-service-types==1.2.0 oslo.config==5.2.0 oslo.i18n==3.20.0 oslo.utils==3.33.0 oslotest==3.2.0 pbr==2.0.0 pep257==0.7.0 pykerberos==1.2.1 pyparsing==2.2.0 python-mimeparse==1.6.0 python-subunit==1.2.0 pytz==2018.3 PyYAML==3.12 reno==3.1.0 requests==2.14.2 requests-kerberos==0.8.0 requests-mock==1.2.0 requestsexceptions==1.4.0 rfc3986==1.1.0 six==1.10.0 smmap2==2.0.3 stestr==1.0.0 stevedore==1.20.0 testrepository==0.0.20 testresources==2.0.0 testtools==2.2.0 traceback2==1.4.0 unittest2==1.1.0 urllib3==1.22 wrapt==1.10.11 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.2647943 keystoneauth1-4.4.0/releasenotes/0000775000175000017500000000000000000000000017037 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3287945 keystoneauth1-4.4.0/releasenotes/notes/0000775000175000017500000000000000000000000020167 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/.placeholder0000664000175000017500000000000000000000000022440 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/1583780-700f99713e06324e.yaml0000664000175000017500000000056300000000000023716 0ustar00zuulzuul00000000000000--- features: - Added a new OidcAccessToken plugin, accessible via the 'v3oidcaccesstoken' entry point, making possible to authenticate using an existing OpenID Connect Access token. fixes: - > [`bug 1583780 `_] OpenID connect support should include authenticating using directly an access token. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/add-oidc-client-credentials-2be065926ba4b849.yaml0000664000175000017500000000013000000000000030420 0ustar00zuulzuul00000000000000--- features: - Add support for the Client Credentials OpenID Connect grant type. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/add-oidc-discovery-document-support-b07fe54f83286d62.yaml0000664000175000017500000000075600000000000032231 0ustar00zuulzuul00000000000000--- features: - > Add support for the `OpenID Connect Discovery Document `_ into the OpenID Connect related plugins. Now it is possible to only pass the `discovery-url` option and the plugins will try to fetch the required metadata from there. fixes: - > [`bug 1583682 `_] OpenID Connect plugins should support OpenID Connect Discovery. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/add-prompt-to-opt-d083acc357a7f07b.yaml0000664000175000017500000000070700000000000026637 0ustar00zuulzuul00000000000000--- prelude: > Add the prompt parameter to loader Opts features: - The prompt parameter was added to the Opts provided by auth plugins. The presence of the prompt parameter on an Option will indicate to plugin loaders that it is ok to prompt the user for input for this parameter if none is provided initially. Actual implementation of this prompting mechanism will be handled by the individual loaders such as os-client-config. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/add-totp-auth-plugin-0650d220899c25b7.yaml0000664000175000017500000000115500000000000027021 0ustar00zuulzuul00000000000000--- features: - > [`blueprint totp-auth `_] Add an auth plugin to handle Time-Based One-Time Password (TOTP) authentication via the ``totp`` method. This new plugin will accept the following identity options: - ``user-id``: user ID - ``username``: username - ``user-domain-id``: user's domain ID - ``user-domain-name``: user's domain name - ``passcode``: passcode generated by TOTP app or device User is uniquely identified by either ``user-id`` or combination of ``username`` and ``user-domain-id`` or ``user-domain-name``. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/additional-headers-f2d16f85f5abe942.yaml0000664000175000017500000000075200000000000027105 0ustar00zuulzuul00000000000000--- prelude: > Allow specifying additional_headers to the session and the adapter to add headers to all requests that pass through these objects. features: - Add the ability to provide additional_headers to the session and adapter object. This will allow clients particularly to provide additional ways to identify their requests. It will also hopefully provide an intermediate way to handle setting microversions until we support them directly with keystoneauth. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/allow_version_hack-flag-9b53b72d9b084c04.yaml0000664000175000017500000000075700000000000030001 0ustar00zuulzuul00000000000000--- features: - A new flag `allow_version_hack` was added to identity plugins and the adapter which will allow a client to opt out of making guesses at the version url page of a service. This means that if a deployment is misconfigured and the service catalog contains a versioned endpoint that does not match the requested version the request will fail. This will be useful in beginning to require correctly deployed catalogs rather than continue to hide the problem. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/api-sig-error-guideline-handler.yaml0000664000175000017500000000015500000000000027112 0ustar00zuulzuul00000000000000--- features: - Fix handling of HTTP error payloads that conform to the API SIG formatting guidelines. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/basic-http-auth-45bea4298209df75.yaml0000664000175000017500000000041300000000000026214 0ustar00zuulzuul00000000000000--- features: - | A new ``http_basic`` auth plugin is added which enables HTTP Basic authentication for standalone services. Like the ``noauth`` plugin, the ``endpoint`` needs to be specified explicitly, along with the ``username`` and ``password``.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/bp-application-credentials-416a1f8bb2311e04.yaml0000664000175000017500000000050500000000000030364 0ustar00zuulzuul00000000000000--- features: - | [`blueprint application-credentials `_] Support for authentication via an application credential has been added. Keystoneauth can now be used to authenticate to Identity servers that support application credentials. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/bp-system-scope-29e9c597039ddb1e.yaml0000664000175000017500000000064000000000000026337 0ustar00zuulzuul00000000000000--- features: - | [`blueprint system-scope `_] Keystoneauth now has the ability to authenticate for system-scoped tokens, which were implemented during the Queens development cycle. System-scoped tokens will eventually be required to separate system-level APIs from project-level APIs, allowing for better security via scoped RBAC. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/bug-1582774-49af731b6dfc6f2f.yaml0000664000175000017500000000020200000000000025046 0ustar00zuulzuul00000000000000--- fixes: - Fix passing scope parameters in Oidc* auth plugins. [Bug `1582774 `_] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/bug-1614688-c4a1bd54f4ba5644.yaml0000664000175000017500000000051300000000000024753 0ustar00zuulzuul00000000000000--- prelude: > HTTP connections work under Windows Subsystem for Linux fixes: - > [`bug 1614688 `_] HTTP connections were failing under Windows subsystem for Linux because TCP_KEEPCNT was being set and that environment does not support such override yet. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/bug-1616105-cc8b85eb056e99e2.yaml0000664000175000017500000000055500000000000024770 0ustar00zuulzuul00000000000000--- fixes: - > [`bug 1616105 `_] Only log the response body when the ``Content-Type`` header is set to ``application/json``. This avoids logging large binary objects (such as images). Other ``Content-Type`` will not be logged. Additional ``Content-Type`` strings can be added as required. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/bug-1654847-acdf9543158329ec.yaml0000664000175000017500000000020400000000000024710 0ustar00zuulzuul00000000000000--- fixes: - | The ``X-Service-Token`` header value is now properly masked, and is displayed as a hash value, in the log. ././@PaxHeader0000000000000000000000000000021300000000000011451 xustar0000000000000000117 path=keystoneauth1-4.4.0/releasenotes/notes/bug-1689424-set-adfspassword-endpointreference-f186d84a54007b09.yaml 22 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/bug-1689424-set-adfspassword-endpointreference-f186d84a54007b0000664000175000017500000000076400000000000032426 0ustar00zuulzuul00000000000000--- prelude: > Allow setting EndpointReference in ADFSPassword features: - > Add the ability to specify the WS-Policy EndpointReference used in the ADFSPassword plugin's RequestSecurityToken message via the 'service-provider-entity-id' option. Also added 'identity-provider-url' option which was required, but missing from option list. fixes: - > [`bug 1689424 `_] Allow setting EndpointReference in ADFSPassword. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/bug-1733052-1b4af3b3fe1b05bb.yaml0000664000175000017500000000044000000000000025062 0ustar00zuulzuul00000000000000--- fixes: - > [`bug 1733052 `_] Now the version discovery mechanism only fetches the version info from server side if the versioned url has been overrode. So that the request url's path won't be changed completely. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/bug-1766235wq-0de60d0f996c6bfb.yaml0000664000175000017500000000030700000000000025414 0ustar00zuulzuul00000000000000--- fixes: - | [`bug 1766235 `_] Fixed an issue where passing headers in as bytes rather than strings would cause a sorting issue. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/bug-1839748-5d8dfc99c43aaefc.yaml0000664000175000017500000000075700000000000025235 0ustar00zuulzuul00000000000000--- features: - | [`bug 1839748 `_] Keystoneauth now supports MFA authentication and Auth Receipts. Responses from Keystone containing and auth receipt will now raise a ``MissingAuthMethods`` exception which will contain the auth receipt itself, and information about the missing methods. There are now also ways to easily do more than one method when authenticating to Keystone and those have been documented. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/bug-1840235-ef2946d149ac329c.yaml0000664000175000017500000000111200000000000024673 0ustar00zuulzuul00000000000000--- features: - | [`feature bug 1840235 `_] Adds ``connect_retries`` to Session.__init__(), that can then be used by projects when creating session objects, to set the required number of retries for new connection requests. This would specifically help avoid a scalability issue that results in number of ConnectTimeout errors when doing endpoint discovery and fetching roles using an auth plugin under heavy load. This still allows for it to be overridden per service with the adapter interface. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/bug-1876317-1db97d1b12a3e4b4.yaml0000664000175000017500000000033300000000000024746 0ustar00zuulzuul00000000000000--- fixes: - | [`bug 1876317 `_] The v3 authentication plugins now attempt to add /v3 to the token path if it's not present on the authentication URL. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/cache-trailing-slash-3663c86cd9754379.yaml0000664000175000017500000000052200000000000027057 0ustar00zuulzuul00000000000000--- fixes: - | Fixed an issue where https://example.com and https://example.com/ were being treated as different urls in the discovery cache resulting in a second unneeded discovery call when someone sets an ``endpoint_override`` that didn't match the trailing-slash form given by that service's discovery document. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/cleanup-session-on-delete-1ed6177d4c5c1f83.yaml0000664000175000017500000000063200000000000030257 0ustar00zuulzuul00000000000000--- fixes: - > [`bug 1838704 `_] When consuming keystoneauth1.session.Session, if a requests session is not provided one is created. The Session used for requests may result in a ResourceWarning being generated if it is not properly closed. The code has been updated to close the session correctly when the Session object is deleted. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/client-side-rate-limiting-dec43fc9b54f5b70.yaml0000664000175000017500000000072700000000000030405 0ustar00zuulzuul00000000000000--- features: - | Support added for client-side rate limiting. Two new parameters now exist for ``keystoneauth1.adapter.Adapter``. ``rate`` expresses a maximum rate at which to execute requests. ``parallel_limit`` allows for the creation of a semaphore to control the maximum number of requests that can be active at any one given point in time. Both default to ``None`` which has the normal behavior or not limiting requests in any manner. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/collect-timing-85f007f0d86c8b26.yaml0000664000175000017500000000051300000000000026130 0ustar00zuulzuul00000000000000--- features: - | Added ``collect_timing`` option to ``keystoneauth1.session.Session``. The option, which is off by default, causes the ``Session`` to collect API timing information for every call it makes. Methods ``get_timings`` and ``reset_timings`` have been added to allow getting and clearing the data. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/drop-py-2-7-f90c67a5db0dfeb8.yaml0000664000175000017500000000030500000000000025407 0ustar00zuulzuul00000000000000--- upgrade: - | Python 2.7 support has been dropped. Last release of keystoneauth to support python 2.7 is OpenStack Train. The minimum version of Python now supported is Python 3.6.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/drop-python-3.5-362bb9d47f830353.yaml0000664000175000017500000000007200000000000025720 0ustar00zuulzuul00000000000000--- upgrade: - | Python 3.5 is no longer supported. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/expose-endpoint-status-6195a6b76d8a8de8.yaml0000664000175000017500000000027200000000000027754 0ustar00zuulzuul00000000000000--- features: - | Added a 'status' field to the `EndpointData` object which contains a canonicalized version of the information in the status field of discovery documents. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/filter-versions-service-type-763af68092344b7a.yaml0000664000175000017500000000037600000000000030713 0ustar00zuulzuul00000000000000--- features: - | Added ability to filter the results of ``get_all_version_data`` by service-type. - | Added ``get_all_version_data`` to ``adapter.Adapter`` that uses the adapter's ``service_type`` to filter the version data fetched. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/fix-get-all-version-data-a01ee58524755b9b.yaml0000664000175000017500000000037000000000000027715 0ustar00zuulzuul00000000000000--- fixes: - | The docstring for ``keystoneauth1.session.Session.get_all_version_data`` correctly listed ``'public'`` as the default value, but the argument list had ``None``. The default has been fixed to match the documented value. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/get-auth-ref-7418e13bd0942060.yaml0000664000175000017500000000024500000000000025331 0ustar00zuulzuul00000000000000--- fixes: - | Fixes missing ``get_auth_ref`` call for the ``none`` and ``http_basic`` authentication plugins. The implementation simply returns ``None``. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/global_request_id-per-request-bd66c7e0f1a71d9f.yaml0000664000175000017500000000033600000000000031374 0ustar00zuulzuul00000000000000--- features: - | The ``X-Openstack-Request-Id`` header can now be set per-request via a ``global_request_id`` kwarg to ``Adapter`` and ``Session`` request methods (``request()``, ``get()``, ``put()``, etc.) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/improve-http-error-handling.yaml0000664000175000017500000000014300000000000026420 0ustar00zuulzuul00000000000000--- fixes: - Add logic to handle HTTP error responses that do not conform to a known schema. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/ironic-discovery-fe41793ef97027bf.yaml0000664000175000017500000000024000000000000026573 0ustar00zuulzuul00000000000000--- fixes: - | A workaround for misformed discovery documents was being applied too soon causing ironic discovery documents to be mistakenly ignored. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/ironic-microversions-a69bf92ab21f0cf5.yaml0000664000175000017500000000015000000000000027604 0ustar00zuulzuul00000000000000--- fixes: - | Fixed support for detecting microversion ranges on older Ironic installations. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/ksa_2.2.0-81145229d4b43043.yaml0000664000175000017500000000040300000000000024252 0ustar00zuulzuul00000000000000--- fixes: - > [`bug 1527131 `_] Do not provide socket values for OSX and Windows. other: - Added a betamax fixture for keystoneauth sessions. - Added a RFC 7231 compliant user agent string. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/microversion-header-support-901acd820a21d788.yaml0000664000175000017500000000032700000000000030664 0ustar00zuulzuul00000000000000--- features: - Added support for specifying a microversion to use on a given REST request. The microversion can be specified on session request calls and a default can be set on Adapter construction. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/noauth-discovery-c26d82a32c36d41d.yaml0000664000175000017500000000021600000000000026562 0ustar00zuulzuul00000000000000--- fixes: - | Fixes ``get_api_major_version`` for non-keystone authentication methods when the provided endpoint is not versioned. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/none-auth-dab13ab9af6f5c86.yaml0000664000175000017500000000071200000000000025406 0ustar00zuulzuul00000000000000--- features: - | A new ``none`` auth plugin is added with purpose to simplify loading clients from configuration file options. It does not accept any arguments and sets the token to 'notused'. It does not have any endpoint/url associated with it, and thus must be used together with ``adapter.Adapter``'s ``endpoint_override`` option to instantiate a session for client to a service that is deployed in noauth/standalone mode. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/oslo-config-split-loggers-6bda266d657fe921.yaml0000664000175000017500000000013300000000000030305 0ustar00zuulzuul00000000000000--- features: - | Added ``split-loggers`` option to the oslo.config Session options. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/retries-limit-dbaedcb3207934ae.yaml0000664000175000017500000000027500000000000026274 0ustar00zuulzuul00000000000000--- fixes: - | The retry interval for retries enabled by ``connect_retries`` and ``status_code_retries`` is now limited at 60 seconds. Previously it would grow exponentially. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/retries-options-99e4dbc240941557.yaml0000664000175000017500000000031500000000000026306 0ustar00zuulzuul00000000000000--- features: - | The Adapter parameters ``connect_retries`` and ``status_code_retries`` can now be set via configuration options ``connect-retries`` and ``status-code-retries`` accordingly. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/retry-authenticated-discovery-19c4354ff983f507.yaml0000664000175000017500000000114300000000000031140 0ustar00zuulzuul00000000000000--- fixes: - | Retry version discovery with auth token when the initial request throws 401 Unauthorized. There are some services that are erroneously defaulting to authenticated discovery, and this allows discovery to work properly on them. upgrade: - | If keystoneauth and openstacksdk are both in use and keystoneauth is upgraded to this release **before** upgrading openstacksdk to ``0.36.1`` or later, creation of ServerGroup objects with policies and use of Ansible Inventory could be adversely affected. See https://review.opendev.org/#/c/685999/ for more details. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/retry-delay-68d0c0a1dffcf2fd.yaml0000664000175000017500000000031000000000000026017 0ustar00zuulzuul00000000000000--- features: - | Allows configuring fixed retry delay for connection and status code retries via the new parameters ``connect_retry_delay`` and ``status_code_retry_delay`` accordingly. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/serice-type-aliases-249454829c57f39a.yaml0000664000175000017500000000021100000000000026733 0ustar00zuulzuul00000000000000--- features: - | Added support for service-type aliases as defined in the Service Types Authority when doing catalog lookups. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/status-code-retries-75052a43efa4edb2.yaml0000664000175000017500000000033000000000000027242 0ustar00zuulzuul00000000000000--- features: - | Addes support for retrying certain HTTP status codes when doing requests via the new ``status_code_retries`` and ``retriable_status_codes`` parameters for ``Session`` and ``Adapter``. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/support-api-wg-discovery-2cb4b0186619e124.yaml0000664000175000017500000000071000000000000030021 0ustar00zuulzuul00000000000000--- features: - Added support for the API Working Group recommendations on service and version discovery. New methods on Session and Adapter, "get_endpoint_data" will return endpoint metadata including microversion information. Additionally, versions can be requested with a range and with the string "latest", and interface values can be given as a list in case a user wants to express a 'best available' set of preferences. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/user-agent-generation-b069100508c06177.yaml0000664000175000017500000000151200000000000027161 0ustar00zuulzuul00000000000000--- prelude: > Allow adding client and application name and version to the session and adapter that will generate a userful user agent string. features: - You can specify a ``app_name`` and ``app_version`` when creating a session. This information will be encoded into the user agent. - You can specify a ``client_name`` and ``client_version`` when creating an adapter. This will be handled by client libraries and incluced into the user agent. - Libraries like shade that modify the way requests are made can add themselves to additional_user_agent and have their version reflected in the user agent string. deprecations: - We suggest you fill the name and version for the application and client instead of specifying a custom ``user_agent``. This will then generate a standard user agent string. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/notes/version-between-b4b0bcf4cecfb9e4.yaml0000664000175000017500000000025000000000000026755 0ustar00zuulzuul00000000000000--- features: - | Exposed ``keystoneauth1.discover.version_between`` as a public function that can be used to determine if a given version is within a range. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3327944 keystoneauth1-4.4.0/releasenotes/source/0000775000175000017500000000000000000000000020337 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3327944 keystoneauth1-4.4.0/releasenotes/source/_static/0000775000175000017500000000000000000000000021765 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/_static/.placeholder0000664000175000017500000000000000000000000024236 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3327944 keystoneauth1-4.4.0/releasenotes/source/_templates/0000775000175000017500000000000000000000000022474 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/_templates/.placeholder0000664000175000017500000000000000000000000024745 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/conf.py0000664000175000017500000002163300000000000021643 0ustar00zuulzuul00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # keystoneauth Release Notes documentation build configuration file, created # by sphinx-quickstart on Tue Nov 3 17:40:50 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'openstackdocstheme', 'reno.sphinxext', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'keystoneauth Release Notes' copyright = u'2015, Keystone Developers' # Release notes are version independent. # The short X.Y version. # The full version, including alpha/beta/rc tags. release = '' # The short X.Y version. version = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'keystoneauthReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'keystoneauthReleaseNotes.tex', u'keystoneauth Release Notes Documentation', u'Keystone Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'keystoneauthreleasenotes', u'keystoneauth Release Notes Documentation', [u'Keystone Developers'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'keystoneauthReleaseNotes', u'keystoneauth Release Notes Documentation', u'Keystone Developers', 'keystoneauthReleaseNotes', 'Authentication plugins for the OpenStack Identity service.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] # -- Options for openstackdocstheme ------------------------------------------- openstackdocs_repo_name = 'openstack/keystoneauth' openstackdocs_auto_name = False openstackdocs_bug_project = 'keystoneauth' openstackdocs_bug_tag = 'doc' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/index.rst0000664000175000017500000000034300000000000022200 0ustar00zuulzuul00000000000000============================ keystoneauth Release Notes ============================ .. toctree:: :maxdepth: 1 unreleased victoria ussuri train stein rocky queens pike ocata newton mitaka ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/mitaka.rst0000664000175000017500000000023200000000000022334 0ustar00zuulzuul00000000000000=================================== Mitaka Series Release Notes =================================== .. release-notes:: :branch: origin/stable/mitaka ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/newton.rst0000664000175000017500000000021600000000000022402 0ustar00zuulzuul00000000000000============================= Newton Series Release Notes ============================= .. release-notes:: :branch: origin/stable/newton ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/ocata.rst0000664000175000017500000000021200000000000022153 0ustar00zuulzuul00000000000000============================ Ocata Series Release Notes ============================ .. release-notes:: :branch: origin/stable/ocata ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/pike.rst0000664000175000017500000000021700000000000022021 0ustar00zuulzuul00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/queens.rst0000664000175000017500000000022300000000000022366 0ustar00zuulzuul00000000000000=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/rocky.rst0000664000175000017500000000022100000000000022213 0ustar00zuulzuul00000000000000=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/stein.rst0000664000175000017500000000022100000000000022206 0ustar00zuulzuul00000000000000=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/train.rst0000664000175000017500000000022100000000000022201 0ustar00zuulzuul00000000000000=================================== Train Series Release Notes =================================== .. release-notes:: :branch: stable/train ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/unreleased.rst0000664000175000017500000000016000000000000023215 0ustar00zuulzuul00000000000000============================== Current Series Release Notes ============================== .. release-notes:: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/ussuri.rst0000664000175000017500000000020200000000000022415 0ustar00zuulzuul00000000000000=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/releasenotes/source/victoria.rst0000664000175000017500000000021200000000000022704 0ustar00zuulzuul00000000000000============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: stable/victoria ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/requirements.txt0000664000175000017500000000160400000000000017633 0ustar00zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. # All additions to this file must have significant justification. # NOTE(morgan) At no time may any oslo library be added to the keystoneauth # requirements. The requirements for keystoneauth are very tightly controlled # to ensure we are not pulling in a ton of transient dependencies. This is # important from the standpoint of ensuring keystoneauth can be used outside # of openstack-specific projects (allowing interaction with openstack APIs) # where oslo and associated transient dependencies are not desired. pbr!=2.1.0,>=2.0.0 # Apache-2.0 iso8601>=0.1.11 # MIT requests>=2.14.2 # Apache-2.0 six>=1.10.0 # MIT stevedore>=1.20.0 # Apache-2.0 os-service-types>=1.2.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631897232.3367944 keystoneauth1-4.4.0/setup.cfg0000664000175000017500000000515700000000000016177 0ustar00zuulzuul00000000000000[metadata] name = keystoneauth1 summary = Authentication Library for OpenStack Identity description-file = README.rst author = OpenStack author-email = openstack-discuss@lists.openstack.org home-page = https://docs.openstack.org/keystoneauth/latest/ python-requires = >=3.6 classifier = Environment :: OpenStack Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [files] packages = keystoneauth1 [extras] kerberos = requests-kerberos>=0.8.0 # ISC saml2 = lxml>=4.2.0 # BSD oauth1 = oauthlib>=0.6.2 # BSD betamax = betamax>=0.7.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0.0 # BSD [entry_points] keystoneauth1.plugin = none = keystoneauth1.loading._plugins.noauth:NoAuth http_basic = keystoneauth1.loading._plugins.http_basic:HTTPBasicAuth password = keystoneauth1.loading._plugins.identity.generic:Password token = keystoneauth1.loading._plugins.identity.generic:Token admin_token = keystoneauth1.loading._plugins.admin_token:AdminToken v2password = keystoneauth1.loading._plugins.identity.v2:Password v2token = keystoneauth1.loading._plugins.identity.v2:Token v3password = keystoneauth1.loading._plugins.identity.v3:Password v3token = keystoneauth1.loading._plugins.identity.v3:Token v3oidcclientcredentials = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectClientCredentials v3oidcpassword = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectPassword v3oidcauthcode = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectAuthorizationCode v3oidcaccesstoken = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectAccessToken v3oauth1 = keystoneauth1.extras.oauth1._loading:V3OAuth1 v3kerberos = keystoneauth1.extras.kerberos._loading:Kerberos v3totp = keystoneauth1.loading._plugins.identity.v3:TOTP v3fedkerb = keystoneauth1.extras.kerberos._loading:MappedKerberos v3tokenlessauth = keystoneauth1.loading._plugins.identity.v3:TokenlessAuth v3adfspassword = keystoneauth1.extras._saml2._loading:ADFSPassword v3samlpassword = keystoneauth1.extras._saml2._loading:Saml2Password v3applicationcredential = keystoneauth1.loading._plugins.identity.v3:ApplicationCredential v3multifactor = keystoneauth1.loading._plugins.identity.v3:MultiFactor [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/setup.py0000664000175000017500000000127100000000000016061 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import setuptools setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/test-requirements.txt0000664000175000017500000000145200000000000020611 0ustar00zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. hacking>=3.0.1,<3.1.0 # Apache-2.0 flake8-docstrings==0.2.1.post1 # MIT flake8-import-order>=0.17.1 #LGPLv3 pycodestyle>=2.0.0,<2.6.0 # MIT bandit<1.6.0,>=1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD oslo.config>=5.2.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 betamax>=0.7.0 # Apache-2.0 reno>=3.1.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 stestr>=1.0.0 # Apache-2.0 testresources>=2.0.0 # Apache-2.0/BSD testtools>=2.2.0 # MIT PyYAML>=3.12 # MIT requests-kerberos>=0.8.0 # ISC lxml>=4.2.0 # BSD oauthlib>=0.6.2 # BSD ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631897194.0 keystoneauth1-4.4.0/tox.ini0000664000175000017500000000571200000000000015666 0ustar00zuulzuul00000000000000[tox] minversion = 3.1.1 skipsdist = True envlist = py37,pep8,releasenotes ignore_basepython_conflict = True [testenv] usedevelop = True setenv = VIRTUAL_ENV={envdir} OS_STDOUT_NOCAPTURE=False OS_STDERR_NOCAPTURE=False deps = -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = stestr run {posargs} whitelist_externals = bash basepython = python3 [testenv:pep8] commands = flake8 # Run security linter # B110: except: pass # B410: importing etree bandit -r keystoneauth1 -x tests -s B110,B410 [testenv:bandit] # NOTE(browne): This is required for the integration test job of the bandit # project. Please do not remove. commands = bandit -r keystoneauth1 -x tests -s B110,B410 [testenv:venv] commands = {posargs} [testenv:cover] setenv = {[testenv]setenv} PYTHON=coverage run --source keystoneauth1 --parallel-mode commands = stestr run {posargs} coverage combine coverage html -d cover coverage xml -o cover/coverage.xml [testenv:debug] commands = oslo_debug_helper -t keystoneauth1/tests {posargs} [flake8] # D100: Missing docstring in public module # D101: Missing docstring in public class # D102: Missing docstring in public method # D103: Missing docstring in public function # D104: Missing docstring in public package # D203: 1 blank line required before class docstring (deprecated in pep257) # W503 line break before binary operator # W504 line break after binary operator ignore = D100,D101,D102,D103,D104,D203,W503,W504 # H106: Don’t put vim configuration in source files # H203: Use assertIs(Not)None to check for None enable-extensions=H106,H203 show-source = True exclude = .venv,.tox,dist,doc,*egg,build import-order-style = pep8 application-import-names = keystoneauth1 [testenv:docs] deps = -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} -r{toxinidir}/doc/requirements.txt commands = bash -c "rm -rf doc/build" bash -c "rm -rf doc/source/api" sphinx-build -W -b html -d doc/build/doctrees doc/source doc/build/html [testenv:pdf-docs] envdir = {toxworkdir}/docs deps = {[testenv:docs]deps} whitelist_externals = make rm commands = rm -rf doc/build/pdf sphinx-build -W -b latex doc/source doc/build/pdf make -C doc/build/pdf [testenv:releasenotes] deps = -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [flake8:local-plugins] extension = K333 = checks:check_oslo_namespace_imports paths = ./keystoneauth1/hacking [testenv:lower-constraints] deps = -c{toxinidir}/lower-constraints.txt -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt