summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/ext/apidoc.py4
-rw-r--r--doc/source/index.rst9
-rw-r--r--examples/pki/cms/auth_token_scoped_expired.pkiz2
-rw-r--r--examples/pki/cms/auth_v3_token_revoked.json1
-rw-r--r--examples/pki/cms/auth_v3_token_revoked.pkiz2
-rw-r--r--examples/pki/cms/auth_v3_token_scoped.json1
-rw-r--r--examples/pki/cms/auth_v3_token_scoped.pkiz2
-rw-r--r--examples/pki/cms/revocation_list.json2
-rw-r--r--examples/pki/cms/revocation_list.pem20
-rw-r--r--examples/pki/cms/revocation_list.pkiz2
-rw-r--r--keystoneclient/__init__.py39
-rw-r--r--keystoneclient/access.py57
-rw-r--r--keystoneclient/adapter.py8
-rw-r--r--keystoneclient/auth/base.py38
-rw-r--r--keystoneclient/auth/identity/base.py5
-rw-r--r--keystoneclient/auth/identity/v3/__init__.py (renamed from keystoneclient/tests/functional/test_fake.py)23
-rw-r--r--keystoneclient/auth/identity/v3/base.py (renamed from keystoneclient/auth/identity/v3.py)204
-rw-r--r--keystoneclient/auth/identity/v3/federated.py111
-rw-r--r--keystoneclient/auth/identity/v3/password.py88
-rw-r--r--keystoneclient/auth/identity/v3/token.py65
-rw-r--r--keystoneclient/base.py9
-rw-r--r--keystoneclient/common/cms.py29
-rw-r--r--keystoneclient/exceptions.py2
-rw-r--r--keystoneclient/fixture/v2.py33
-rw-r--r--keystoneclient/fixture/v3.py33
-rw-r--r--keystoneclient/middleware/s3_token.py3
-rw-r--r--keystoneclient/service_catalog.py10
-rw-r--r--keystoneclient/session.py61
-rw-r--r--keystoneclient/shell.py19
-rw-r--r--keystoneclient/tests/functional/base.py32
-rwxr-xr-x[-rw-r--r--]keystoneclient/tests/functional/hooks/post_test_hook.sh0
-rw-r--r--keystoneclient/tests/functional/test_access.py47
-rw-r--r--keystoneclient/tests/functional/test_cli.py143
-rw-r--r--keystoneclient/tests/unit/auth/test_identity_common.py7
-rw-r--r--keystoneclient/tests/unit/auth/test_identity_v2.py3
-rw-r--r--keystoneclient/tests/unit/auth/test_identity_v3.py49
-rw-r--r--keystoneclient/tests/unit/auth/test_identity_v3_federated.py96
-rw-r--r--keystoneclient/tests/unit/auth/test_loading.py47
-rw-r--r--keystoneclient/tests/unit/auth/test_password.py5
-rw-r--r--keystoneclient/tests/unit/auth/test_token.py5
-rw-r--r--keystoneclient/tests/unit/auth/test_token_endpoint.py2
-rw-r--r--keystoneclient/tests/unit/auth/utils.py4
-rw-r--r--keystoneclient/tests/unit/generic/test_client.py2
-rw-r--r--keystoneclient/tests/unit/test_auth_token_middleware.py102
-rw-r--r--keystoneclient/tests/unit/test_discovery.py111
-rw-r--r--keystoneclient/tests/unit/test_fixtures.py5
-rw-r--r--keystoneclient/tests/unit/test_http.py14
-rw-r--r--keystoneclient/tests/unit/test_s3_token_middleware.py36
-rw-r--r--keystoneclient/tests/unit/test_session.py140
-rw-r--r--keystoneclient/tests/unit/utils.py12
-rw-r--r--keystoneclient/tests/unit/v2_0/client_fixtures.py7
-rw-r--r--keystoneclient/tests/unit/v2_0/test_access.py7
-rw-r--r--keystoneclient/tests/unit/v2_0/test_auth.py10
-rw-r--r--keystoneclient/tests/unit/v2_0/test_service_catalog.py25
-rw-r--r--keystoneclient/tests/unit/v2_0/test_shell.py6
-rw-r--r--keystoneclient/tests/unit/v3/client_fixtures.py13
-rw-r--r--keystoneclient/tests/unit/v3/saml2_fixtures.py110
-rw-r--r--keystoneclient/tests/unit/v3/test_access.py14
-rw-r--r--keystoneclient/tests/unit/v3/test_auth.py10
-rw-r--r--keystoneclient/tests/unit/v3/test_auth_saml2.py156
-rw-r--r--keystoneclient/tests/unit/v3/test_discover.py6
-rw-r--r--keystoneclient/tests/unit/v3/test_federation.py96
-rw-r--r--keystoneclient/tests/unit/v3/test_oauth1.py6
-rw-r--r--keystoneclient/tests/unit/v3/test_projects.py86
-rw-r--r--keystoneclient/tests/unit/v3/test_role_assignments.py15
-rw-r--r--keystoneclient/tests/unit/v3/test_service_catalog.py53
-rw-r--r--keystoneclient/tests/unit/v3/test_simple_cert.py40
-rw-r--r--keystoneclient/tests/unit/v3/test_users.py2
-rw-r--r--keystoneclient/tests/unit/v3/utils.py10
-rw-r--r--keystoneclient/v3/client.py6
-rw-r--r--keystoneclient/v3/contrib/federation/core.py4
-rw-r--r--keystoneclient/v3/contrib/federation/saml.py81
-rw-r--r--keystoneclient/v3/contrib/federation/service_providers.py104
-rw-r--r--keystoneclient/v3/contrib/simple_cert.py43
-rw-r--r--keystoneclient/v3/projects.py49
-rw-r--r--keystoneclient/v3/role_assignments.py8
-rw-r--r--requirements.txt12
-rw-r--r--test-requirements.txt8
78 files changed, 2183 insertions, 548 deletions
diff --git a/doc/ext/apidoc.py b/doc/ext/apidoc.py
index 60ad23e..545071e 100644
--- a/doc/ext/apidoc.py
+++ b/doc/ext/apidoc.py
@@ -37,9 +37,11 @@ def run_apidoc(app):
package_dir = path.abspath(path.join(app.srcdir, '..', '..',
'keystoneclient'))
source_dir = path.join(app.srcdir, 'api')
+ ignore_dir = path.join(package_dir, 'tests')
apidoc.main(['apidoc', package_dir, '-f',
'-H', 'keystoneclient Modules',
- '-o', source_dir])
+ '-o', source_dir,
+ ignore_dir])
def setup(app):
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 9ab430d..9d931ca 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -18,6 +18,15 @@ Contents:
using-api-v2
api/modules
+Related Identity Projects
+=========================
+
+In addition to creating the Python client library, the Keystone team also
+provides `Identity Service`_, as well as `WSGI Middleware`_.
+
+.. _`Identity Service`: http://docs.openstack.org/developer/keystone/
+.. _`WSGI Middleware`: http://docs.openstack.org/developer/keystonemiddleware/
+
Contributing
============
diff --git a/examples/pki/cms/auth_token_scoped_expired.pkiz b/examples/pki/cms/auth_token_scoped_expired.pkiz
index 46e12b3..5824b4e 100644
--- a/examples/pki/cms/auth_token_scoped_expired.pkiz
+++ b/examples/pki/cms/auth_token_scoped_expired.pkiz
@@ -1 +1 @@
-PKIZ_eJylVllzokoYfe9fcd9TUwGURB7moVlEkG7DIku_sURZxaiA8Otvg5lJJjM1mbnXKkv7azh9-jvf9uUL_YiKquF_JGSPiy8AadqK3wf6uiZa2sYYmrFUUxs7SJIoIAmaylVy4Ercb0_yHklqTu07pEr2i2pr0QzIprKCITU-m8p-7--fez0NOFzGM5RtMvFRO1htyLmNltfj3jGeYZZ41i7wTboPM4By2KGM6ZDNXDdLs0dOfcVy3WG2zgwJZsRPu9DXy7jXHjRJ65GsdIYDOfrtXVlhwCY3Z5scMV6mnTVJPxJpfFDvInWZEy9tI9Uq49seQzw-jQ7mjVnlNoGnnwHxJgMTH9xyPDH0btQSdXyAZ3yuLJA9AdA1W45Xodcqo2rZJEuhAaPx9YGC-DiPq7JL1LKNMspE5dlIvQo7u6MvUb8cyDHgXDnwrTRShcMIBrzZOaPUilgVjtHBGkbjaAs86xJ6vLstuolRuLKYWK5bY1B63M87I4cN9dcc4CHmjFlwNYaACXJSIY5kAYdYNJgD9rbzwAk6UuFqI5dZkNMdJ0m1bGSkl4QrWzBRlfSeeMkx4vinqNpmN1_wDPHHe19_ywKMNP47C5EFQXU9BqxrOzf56mRldZts0SJHm033lObXTb6f4SFok1xpcW5luAoGUul5MGwHgGXEEidNcS5WARfMN56e4Ty4Yk7Jdi4zqkMls7qIK5tkhXeRWjbhLUojn6oDqKZ8rG5v2lfCORm1HulVNGi8D_YDBVz9qBr4S9l4lBf96K9vSoBRiv-jBPg1ssj-mXOLHmwcbfTuHznsvb9Cj00Jt83ASJUmxoXY00kHmoVNQDfG-Kf5_w7wBjTWC6Kyx6i8sQO_194c2TXIXnBY1TiS0wiocI4dxKDBLTeqwgGkkpzkez6oNA7nyiy4ZearQ-cT3bhyD6EnNA7Hv0pMGaluT5mfgevqaMrxKmWSFXww-sUV5fHIgkqnPGxkWsQkgUP2_JehDj6L9c_UAH9z35-vqwtgkofWN7IS2zFwYlY409_TVESXl2RUKVmV3atKecSxXeJb38MdfIv3qBK4EdGQbgXzO-ANiNqWReSVzUefgY9OQzRFsRPT62jU9rMi2w-KgPeSfKbIL8A7kNDaHnr4syxkkQw7mis9Xf8Q9uDP4p72gYN1TFZF9iH0T4D45ZCoQkOoA28vugPxtde-OP5_31Cm9W4CcJQJDMS02tCG8gMDWihoS9t-Kxrj_14rmfetj405tx-7FnjtSn3EXcs3yd7K2XQAPe01O9-xuQHTmsgmH-71ij6BXOm-sHNUCcZ1t9-vVLhXRKSpBf0-I7PugAQD2TXNpdIpLDLPnWROa1XpdHc7KAaChQrZrSKJSDIZ5ark0BT32BVh7EguZkFU8XxCY4DStJEIbw-nSHdmYhmVmAk8fEKW0sndBGwoXWp8d7yjlDSZ3sYVOq3o0Ao8OpHw8cxqo8qF0Qh0uAEZldCTQdkiUZsOgldkgC3nnkkmOqGXNHRWkCA9TeyC5bqbyzCtO9l8Pz8pomhCOgop0HXvrfV12IPuvmALFr_AU6o_S-v1oAaeQUcHbr15Rn4OD6VZSFYdtufoqPMsL89wj0K25g_ZU_60UYBRtY1w2iV2hA7IYTJPix4C88LthodjcV607Hx21-1iR25K7U6y187m8e6lWnPPtEOJ8pYDQ8JD3_OlRajb98938SU89ZZhyGSh1aUeP0JGmKsVU5GFW7TUR9za12xhMdMb_iS36QvYVQK_WdOYUZTL6epZ0kJ07XZPLm60Ter1cCqokwss7Z48KUzI4t6QntKGXPZmcT8_QhFs933Irk_YM2XphFnpBdbyQ3wRnbI4PS7tPGU3Dxx7ybgkVDsY-mFzUdh43dv1Mr5USQQa2nfnd4-rcpFfVvDrVzANsAqW34bZfwG2EZ6e \ No newline at end of file
+PKIZ_eJylVtuSmzgQfddX7PtUKiDsGfOQBy4yhrHEcLd4MzAGZLA99pjb16_Ak2QySW2yu66iDC3RnO5zulufPvGfigyT_KVhb3z4BLBpmnZOrcdjbBZNShQn1Y7c9jho_JdqioM6zVdWah6c9RxrBtM0dZ8amvdieGYiAd1BK2XLjSxHeU6F594qKCRVKuHSLtUH8-A2WxheTXbM-doplYgYR-6Obhy-rpQAM6XFpdBiT-jspdNj_9gR_dgS8ViuNaWMN0W73VhV2pv3pmb2WEft2lcgv_pQRwKwmSPZDAtRaV5MzTrF2rjRahNjyeKoaBLDrdLbmhBH8yI5ODdkdXilkXUBcTQZhPQQVuMXt9ENWmaMG-bCBlZ77E0O-LNYjaHwsKqkXl6zpXwFo_Ftwz7eEJbWVZsZVZOUHIkxFxOjk3dey1_ieTnEJwpDnW7cIjHkw-gMRNKl5NB4XuVTcnCH0TjaaOS-bqN5GOzbCdF25QqpfmzWA-pJP2vXTLnyfM0AGVK4lmi3HqhAWVxjGJcUYhEPzkCiYEZ92sY1qW29KinjK35WmOWIyKpiWDVggqpZfRxlpwTOn5I6KG-5mAvxZoy7-0cUYITx31GoIqB1d6Ji6Pk3-o7Zym3tctFg35SmOLVZZ7NcIgNtMoYawtyS1HSIa4vRIRgA0bEY-0VBmFpTSGd2ZJWE0Y5AVO5CYWSHU-a2Cayu2YrsEqO6bm8qTTacHcA5nadGcOO-li_ZyPUIr-aiiT7YD9zh6kfWwL-kbY7Zvh_z9ZUJMFLxf5gAv_asin-W3H0PbN8cs_tHCXufr20kFjEMSjBC5YXxGnvTlw68Cq-UL4z65_X_zuHN0dgvYkM8JdUNHfhn7p0R3RV7C0gME8aMK6AmjPhYwENY2QaCABsxi1k-p7UJCUMSvVXmW0JnE9y0Dg_bSL76cP5GMUdkhD1HfgFhaOGpxutCyFbK_bpfdJilIwpOHbq3dd7ENBlib_ZLqYPfaf13bIB_E-_P4VoymOjh_S1eqc0onFSUL_z_PDXR5Ws2spStqvaNJZZAsc027je5g696T2oZjh7X2q1hfnN4c8Rty30SVdePOQMfk4Z5iRI_5eGY3PYzI8EHRsB7Sn7HyC-ctyDjvX0bkd9VoYh1peW10vPnH2QP_kz3fA4c3FO22pcfpH8G8aYaMkO-xjyBtxfDId6Yb3NxvH8_UKbn3eTAR5MzkPJuwwfKDwh4o-AjLfjaNMb73qyE96NPTGHYj1MLvE2lPoFd9Z2yan9LJm25bPfUL2oupAEzZ06ZVZCB90-2rLGfQkD9jDdR3H3sgxMyDvOtrL9-ucY6qWMDzWJWFHgw-XSOzJ76Ka8Fs4u5PEnNJcob9s8D9S2Ug5i9TyT4Hs_09Y5vkHe-oSnpsc3zlaHkSMWmsefXM3aOraZQPXScJWqRiJ1LCzRnMhiotcJgQGus7A1FDJCmYs0RUIeY4qg5CVUl9bWQiEk9n2dcdDw8D6uKAabNBbZ8Sa2Sigg0ImfsolZvJ8dr1Bbrb1T7qMIa_nY-4scjCygujfgZaJ5KbpPUoZKMjg43R-ta7uMBBVg1J1RKh9cBDC9xqfrbKLvyw4nGHaBWbenysZ3pSnFsdef9iQ2pqqPwwxdShifbKSSRDulneAkzL_1s95acEXAHC8PNXUdeP_Qrz9qeXvW6znWfvOrq4irIun8XtKKPVnfijJEHMs9DT9RWUmE9KjynDFjbJfPxS7Mx0iWWFs9RWj46L3Z3V587XM1PWwNCXoTJ2UNDv1d9vvuz9tLXV_kxDWYYgqHYd8_N6XzoH2b-xWpw0y_gJtAsxErjRb5G4RbKz_YBO2VeLXPDpyxSbVs8N2ZTBVgCiztMAyErXrdbb7N_Me8fcjVnq9dBsKTF4alod0-SuyzE3LNe81ZdFU6TCv48WWXN-hB7O_B8DmemaSyebLh7CO6kaJFerYooCIa2zek7UjVGq6ck30Okq4_3s0OV2WpyLwkwwsqXL2A6MSOifz89_w1E1sSW \ No newline at end of file
diff --git a/examples/pki/cms/auth_v3_token_revoked.json b/examples/pki/cms/auth_v3_token_revoked.json
index 96b47c6..f2ddf29 100644
--- a/examples/pki/cms/auth_v3_token_revoked.json
+++ b/examples/pki/cms/auth_v3_token_revoked.json
@@ -104,6 +104,7 @@
],
"issued_at": "2002-01-18T21:14:07Z",
"expires_at": "2038-01-18T21:14:07Z",
+ "audit_ids": ["ZzzZ2ZZYqT8OzfUVvrjEITQ", "cCCCCCctTzO1-XUk5STybw"],
"project": {
"enabled": true,
"description": null,
diff --git a/examples/pki/cms/auth_v3_token_revoked.pkiz b/examples/pki/cms/auth_v3_token_revoked.pkiz
index b6c8eb5..cf27adf 100644
--- a/examples/pki/cms/auth_v3_token_revoked.pkiz
+++ b/examples/pki/cms/auth_v3_token_revoked.pkiz
@@ -1 +1 @@
-PKIZ_eJydWMl2o8oS3NdXvL1PHyMQbrN4CyYh1KrCIAZV7QRYQAEarIHh618iyd0euu91P3vhIwxZGZkRkYm-fYMfzbRs8h8dL4YP3xC2bew2dPZjy-z8nBDVTfQtXCOGrhuGqquu2eq-OtWy4MXIsG5xXdcr1dIXe2thxxIyXHOqruBiaZpZtlWeu5kQi8cqLuwHe3oo4igUVpZSJh18nhyKxJr0iZSek9otlosmQ_Zmdo6tsLf5NoukQ7GK3MIp1IJxtWe1lzsWHVMfd8SoClybErZYgSPakp7lRLRFhHuzmOtqEdeTI1vAKfqsi8W2wouZMte173ZdlUNwu6YNFllJ_bx2LLPH3JUpn-WI9FpN-aTGfiJSPy0Ix61dNPCAnMdRcMkmqZUD8-1iHQoHOKB6nmq7pA77pTgR0CU6txvcZ0dsmEfs5wHm5gP23QdspKtLsI0GWe0qKg3w3mS18SoEqZ_SibJjxhUKs5QjjarTcAMUdMf0C6wyFkf5KpLXUKN3GaJLwW4PLcXLxdbeXFOF4AUU-HJaOp2N2GJ40KsSkXSrpSasIuV0gRBvwkOsv8edWuGJRrLwISinSy-PLWXz2jXEIrlMLGUXb7w3rZQFtpzVNCLVtQOTMh5gXeoRdvEV1jadeg1AeDxj35bmXD1hfdw6PJNIT88pN8-EewWpKfABTu6Dnhh4xPw8Jxw6J9KxE80KRDiQQrwWEFqzGXBdKzyrmFid41I5AT-G9G8FtXvKw4r4gUBrUiPsmwLjtuAYpAKCtLQOJOqrI2yw8g2ZTlCTjtUTfiPbALlKoGYI8AzRR0ndXIq3mnpCYmzP897sSDduLtD87Zj0iTiXaDvvqUA5q4GVBRXxCKjs9iQKgO-0YZCSA5ynHP7lp_m1aDcoxZXmLEp3sSg_xXVwgY1exUN8L6e12zELC4TDrzXLiQXi4WVLfFceRMUikg-EWwMfBpINMFBSs5yKN85PQyBOxX_Xrj91C321XX_qFvqHdoH2TYlFoHvLbikvBeyXogPBiQgQDYADHYQ2VhWDdg3uAuqSE-tGZZBwGo2qq3Bu6uOBBArl0AEOwTvHdyWEIRIW4bfPKyci0AdTpD3JP3rCz4CDJsDqXiWA_l8NvBYV_apqCSnZgwb-htYywtweqgv8Lke4LwViQCAjkGmNBSdiJTESAdyqY8XvaY3e8vqLtJaBXN1A6wEa-jeq_rJ5uyE-7Wnt1QS0QKywBofO0eDxOAKqRrZI-lkBvX1H1X9jKvozVb9WVPTRLEgf1kDdnAyF9F0BZNw7MI0GWjMf0u5tkflVTm_kQ2_Zt4pGORPfY8d-yDGf5DQKRMptCRQ6ZpY5wnXQgPGIyLECGRs53IA_jrhNbE1OA_5bTcDmvSYWoY1TPAyYeKgT-lgoGnnHVTS-BEuXs8OVkbmQTtWHeffYYp4MNYKWmg-OkUiI6IqIF-NPVvVVp0L_2v_IK5hR1cTXoP9UIgYDGYOCDbMF9-oRTGa4AAZi_bn_N5HBpGo-QUN_wvZVaOhvXfgVmhMFsHBgAZEaj8FdeugvuG_FKccy4yH8VQXHzwtnWCz-gdro71zYHah9wotHEXwSjA0gwHzjxMeg-XAYbSIsUZzxDPwAxMVNiV6pfoMyvq08V134olyh96Y5KRnXCmJ4JaQOPumVAAvaqFXYciE4eKZIKrD9zt6M0stkSqdVc6MuhzWmSZfeByee1cRQBVj5CscYppQHGWcy2H1Be7OBLc0GkaidE4X8oxPHtSLSnxM6PLGpdh44cV06jxd7Qx_Gds6s0Q5a-BVr66F2I0Q6IEz3uVDBm0K9g1SbAvHVdphKOILJRLlXYd8bPG-Mh-WiZjUzsoHno9ch8nleXJ0ZpH9AX-PAQOFAAifuP7J18MTFn-iKRRdmRgB7sT1kBgPVHWPRHjmWV5EozKkfjBGxYJxz9a-c-G2m6H2qIBBDBbriz05c_X68AQR7TEVXGEYb6VUZWyBf0Rw5httR3-4GBwYNNA4Ijemf1wDYkbwK0t6l0_IdftqHFzozIwBNuB1ABTYOexMF9WKwf2BlTUrE_OCjG7-wZdWn1pDq64lhz5b2bdX3znE02b2-Ev3c0n5t7BUsmBeMYGGjW99vD1XK5dp1Ab0eil5fc9iSnVdiWC6l4bTZ7ca3U-vTPZd3B3Rb43fggiNw4DVd3jjA1QYXQoMXQutM3A643xJj25DRtlgvhTbLpj_glVFDbWZrqq9rO0PXNtz8gdXMUkeBqXlYI745x2p5_ZxjPQxxq_fqTMtIqKmZr5ZEQ9izG1OlRui6U7Op_DSST89LBi8VQWty1b3evPX1QGlgsfJTa8JXvelh9fESGOktthdiKCcSFKoO2pmvci0r93lZWEojaLprRpP6WD0vCbyQypVrXQL1l0CdfIZVN2knhrq4noR9fUSq2KJZIFabuA5LNRtOSyxtcXDUxl5hVfj52gtvvRpS3UDVoBiquuh984me--eJMt_nVTEzVnshuxt50fi-dsc92SpHs0sPZbT08e5YvWiOtHLRXVn6TVUHLHQZkQ4LWxTXE-kpLTbzp9PLTLGUqoiWEiHa82QMLpC_3Av3S76-8_O-mMoZKn1FBCHsvstjfVZHrf2UqcZTnobnY_DUKdLq-zq_f1nNrbGftet6v3qqLX8_K7P0R7Dw7hYoP_Yvd4GmlHdPiqATMkqeor0yndE6fBiP1zVP7rvs5Qd-8KarQD2cO_V7WoAW9pP9JnyMErRbqydV1kl3mC-yyS5Re1UPtfu7yVY6cGbdBcXOUB7WXWhYrMuOfRo-5vxRm965rRlsHjxk3I3X3_eqoXf3e9iAnqszK5Z7Khz225fKUf-LLt9TmMT49Z3F_wD_upxB \ No newline at end of file
+PKIZ_eJydWEt3ozgT3etXfPucPgPCpOPFLHgZ47FEwDws7QxODELYTvzA8OunsJ3uPDrT6a9z-uSE4FLdqntvlfLtG_wzHdej_7PIrP_hGyKeR4qGTf7ZcK845tQIcmsDzx5sy7LHgWUEzsmKjLG5ip_tFbFcYVnWNnCt2ZM78zIN2YEzNhbwcBM7q9WT-dBOlAzvZVZ6t954V2ZpoizcYZW38PNoV-buqMu15TGvg3I-a1bIW0-OmZt0ntisUm1XLtKg9Euj5MLoeB0WvssGLCIttWVJakcjLi9Jyk604wXFHkakc8qpZZRZPdrzGZxiTdoMnySZTYZTy_zu1bLqg3s1awjmFYuK2nedjohAZ2JSINqZNROjmkQ5ZtGypIKcvLKBD-hFlsbnbPJ6uOORVz4myg4OkA9jc5vXSTfHIwWdowuvId1qT2xnT6IiJsK5JVFwS-zl4hxsbUJWW8m0Ht6rrNahRJD6YTkabrl9gcLd4Z6l8tC_AAXdcusMq8qwWixS_RFq9CZDdC7Y9UNzfH548taXVCF4CQU-n7YcT1Q-6z8YyhzTdjE3lUU6PJwhZOtkl1lvcS_d5MBSXXkXVLB5WGTucP3SNcRTvcrd4TZbh69aqSt8PqlZSuWlA6Mq62Gd65G02QXWZjkOG4BwdySRp02FcSDW4OSLlUY7dlwK50hFWNKaAR_g5C7uqE1UHhUFFdA5zAZ-OikRFUAKfCkgtGbd47pUeCI5lsesGh6AH33614J6HROJpFGssJrWiESOwoWn-DaVQJATq2ONRYZKbF69ItMBatLyeiSuZOshyxxqhgBPH13N6-ZcvMU4VHJ7c5x2TkvbQXOGFm0GtMvxVGOnaccUJngNrCwZJipQOehoGgPfWcMhJR84zwT8KloWl6JdoZQXmvN0uc2wfp_V8Rk2ehEPjcKC1UHLXaJQAV_upKAuiEdUJxoFei8qntKiJ9wj8KEnWQ8D5TUvGL5yfpwAcaT4Vbs-6xb6ars-6xb6j3aB9h2Np6B71zsxUSkkqrAPwSkGiDbAgQ5CG6Xk0K7eXUBdeu5eqQwSXqaqvAjnqj4Ra6BQAR0QELz1o0BDBCIRDF9dIf2UQh8czDpavPeEHwF7TYDVvUgA_b8aeCkq-lnVClLyeg38Ca11RITXVxf4XamkqxRqQyA71llNFD_lFbVzBdyq5eWvaY1e8_qLtNaBXG1P6x4a-h1Vf9q819CIdawOawpaoG5Sg0MXqPd4kgJVUw_TblJCb99Q9XdMRZ9T9WtFRe_NgnZJDdQtaF_IKFBAxp0P06inNY8g7c7DPJIFu5IPvWbfIlULjt9iJ1EiiBgVLI0xE54GCh1w11FJHTdgPBj5bqwTu4AXyPsRt87c0aHHf60J2HzYZBjaOCb9gMn6OqH3hWJpuF-kg3Ow5XyyuzCyUJZj43ba3p2IyPsaQUudW9_ONUStISazwQer-qpTod_2Pw1LbsuaRib0n2nU5iBjULDtnMC9OgSTGR6Agbif9_8qMphUzQdo6DNsX4WG_tSFX6D5aQwLB1EQrckA3KWD_oL7SsEE0blI4Luh-FFR-v1i8R_URn_mwkFP7QOZ3WHwSTA2gADzTdCIgOaTfrRhWKIEFyvwAxCXcDR2ofoVyuC68lx0EWFdoremOaq4MEtqhxWkDj4ZVgAL2mhK4gYQHDwTUwm233prdXmeTMuxbK7UFbDGNMt5-M6JJzW1DQVWvtK3-ykVQsYrHey-ZJ3TwJbmgUiM1k8T8d6Js3qI2Y8JnRz42Dz2nLgsnfuzvaF3Y7vgrrqFFn7F2jqonYpoC4RpPxYqflWoN5BqR6GRceqnEklhMjERShKFvecNSL9c1Lzm9qrnufoyRD7Oi4szg_R36Gsc6Ckca-DE3Xu29p44-4yuBAcwM2LYi70-MxiowYBgT_XdUNI0KVgUDxB1YZwL44-c-HWm6G2qIBDbALqSj04sfz3eAII3YDhQ-tFGO0MnLsgXO6pvBy2LvLZ3YNBA44PQuPVxDYAdKZSQ9nY5rt7gZ11ypjO3Y9BE0AJUYGO_NzFQLwH7B1bWtEI8it-78TOfy27p9qm-nJh0fO5dV_3wmKWj7cuV6MeW9nNjl7BgnjGChanXvl8_JIfnZ5cF9HIoernm8Dk_LnBSzbX-tMn1xddT68M757sDuq7xxTKFOvQXj-vQ8OT29kFu2lRueZ4Eg0jb1knCcV5vR7MkDC_0pjaC6WcHmCrJeHtPZipL0p0aq6HO1vn5Wge0hWteIvloWCwv87OFVrdT0MM0cgYosT3ov6P4wtBS2EIeI9cy8k2zWo1dY-WYxHMr-P9Agk1jGcxOgmDkNDAbg11jBcxG8MB1mkkSd86UGJVrqLFjmcQKFOfkCCMwVzQxjTyyEqpmta4vQUCgxBkxjfO7yCrIJNJMmUmqgNqeSeg0dnM-aeo0xfRHSyNHEov8uPLCjXdihCxFUFU916BNdWJkfaD1JdC0Hra8c2JieueTjBOZxjjZ8dKMFunywFO4V8NhyGzY6J9mYBvFprGD15dwxzQDA-7TjtGOxMIPR9FjE37XlON2OLSOB7sjD972Dj3vk-d2KBcPs-i21Uf3T1YNbT7l2DUf13g_cx-GOztaP5Uj9n3HlcmoCOybMtQmpSKD5BQhO1wbnuf8VQxq81BFdydp7pVVfCrNm25YBtSST48zfdt8V7ARb2Ot3DaMTjl_rr2tk_ylofomW_jOatpsRxv_xr9ZVVYs75RsIBUdVHqzte-8gzmYPVZW87zOHruTtDb5Kdb8h0X2qHkomd3WT8Pd6GCnj3KCT-U8NZXn440q0yM7TXLn4K6G08aLk0qtd_e3M3pj4XEi56UbYWMNV1823R3Nm6ySHdnj1nkw_MhV8NPqefKPqdLh-AFnnXW_N-82BSmq2VgeqvvWHAyCv_9G5z-CONT--QeRfwEmaLr4 \ No newline at end of file
diff --git a/examples/pki/cms/auth_v3_token_scoped.json b/examples/pki/cms/auth_v3_token_scoped.json
index 2b76e58..0fd000a 100644
--- a/examples/pki/cms/auth_v3_token_scoped.json
+++ b/examples/pki/cms/auth_v3_token_scoped.json
@@ -15,6 +15,7 @@
],
"issued_at": "2002-01-18T21:14:07Z",
"expires_at": "2038-01-18T21:14:07Z",
+ "audit_ids": ["VcxU2JYqT8OzfUVvrjEITQ", "qNUTIJntTzO1-XUk5STybw"],
"project": {
"id": "tenant_id1",
"domain": {
diff --git a/examples/pki/cms/auth_v3_token_scoped.pkiz b/examples/pki/cms/auth_v3_token_scoped.pkiz
index e39c215..3365dfe 100644
--- a/examples/pki/cms/auth_v3_token_scoped.pkiz
+++ b/examples/pki/cms/auth_v3_token_scoped.pkiz
@@ -1 +1 @@
-PKIZ_eJylWFt7osoSfe9fcd7zzTeAkh0f9gM3EWM3AbnY_SY4Ag2oiVEuv_5Uo5nJZbJ35pz4kA_U6lpVa60q_PYN_nTLdsh_DLwUF98QdhxMNDq_3zMnP6dE81JjD_fmgWGYhmVontUagTbTs_DJzLBhc8MwSss2lo_20klGyPSsmbaGm9yxsmx_-tHNpUR5rpLCuXVmxyKJI2ltT8q0g-vpsUjtaZ-ONue09orVssmQs5ufEzvqHb7P4tGxWMde4RZawbjWs9rPXZuOaYA7YlYFrq0RtlmBY9qSnuVEcRSEe6tYGFqR1NNntoRTjHmXKG2Fl_PJwtD_cuqqFMGdmjZYYSUN8tq1rR5zT6V8niPS6zXl0xoHqUKDTUE4bp2igS-oeRKHQzZpPTmywCm2kXSEA6ofM_2Q1lG_UqYSGqJzp8F99oxN6xkHeYi5dYsD7xabm_UQbKdDVoeKjgQ8kZV_TuLpQdQJiUL9xG1PnmlcnVZKVeKlI0470ViuLhCuX6omw70LRK1ALFZzWrcVM0TV_W4Th-KLh-HamEvi_WTnb-GQD9A2dnRCNFallTLcvH7Ar1KFdOuVLq3jyUmcnuyiYzIb8HO68vPEnuxeuiYyKFN7coBTXrVSldhqXtOYXNOflglAu9Qj6pJLdvvNzG-QW9ydceCMFlw7YWPcujwbkZ6eN9w6E-4XpKbABzi5D3tiYpkFeU44dE6hYzeeF4hwIIVy4QK0ZveSNhCsYkp1TsrJiV0Keq2L01MeVSQIJVqTGuHAkhh3JNckFRCkpXU4ooEmY5OVr8h0goJ1rJ7yK9kE5CqFgiLAI6LLad0MlV3PfCk19-dFb3WkGzcDtGA_Jn2qLEa0XfRUopzVwMqCKlgGKns9iUPgO20YpOQC5ymHt4JNfinaFUpxoTmLN4dEUR-SOhxgoxcSkcAHPngds7FEOLzseU5sEA8vWxJ4qhAVi0nOTKvYggpBtQMMlNYsp8qV87OoSOyK_65dn3ULfbVdn3UL_UO7QPvWiMWge9tpKS8lHJSKC8GJAhBNgAMdhDZWFYN2CXdxdr6a2leeg4Q3QkgDva-ewMMRKJRDBzgE79zAGyEMkbACrz6v3JhAHyyF9iT_IJyXgDvQBCj4RQLof9XAS1HRr6qWkJIjNPAntFYR5o6oLvC7lHFfSsSEQGao0hpLbsxKYqYSuFXHit_TGr3m9RdprQK5OkFrAQ39G1V_2bzTkID2tPZrAlogdlSDQ-dIeDyOgaqxo5B-XkBv31D135iKPqfq14qK3psF6aMaqJsTUcjAk0DGvQvTSNCaBZB27ygsqHJ6JR96zb51LOdMeYsdBxHHfJrTOFQod0ag0DGzLRnXYQPGoyDXDlVs5vAB_H7E7RJ7ehL4rzWpktpvEgXaOMNiwCSiTuh9oWjsP6_j8RBss5ofL4zMpc1Mu110dy3mqagRtNS6dc10hIgxUfBy_MGqvupU6F_7H_sFM6uaBDr0n46IyUDGoGDTasG9egSTGW6Agdif9_8qMpiJzQdo6DNsX4WG_tSFX6C5cQgLB5YQqfEY3KWH_oL7VpxyrDIewX9NcoO8cMVi8Q_URn_mwp6g9gkv7xTwSTA2gADzjZMAg-YjMdoUWKI44xn4AYiLWyN6ofoVyvi68lx0EShqhd6a5rRkXC-I6ZeQOvikXwIsaKNeYduD4OCZCqnA9jtnJ2-GybSZVc2VujxR5Gaz8t858bwmpibByle4pphSPmScqWD3Be2tBrY0B0SidW4c8fdOnNQThf6c0NGJzfSz4MRl6Xwe7A29G9s5s2WxfX3F2nqonYxIB4TpPhYqfFWoN5BqSyKB1oqphGOYTJT7FQ584XljLJaLmtXMzATP5Zch8nFeXJwZpH9EX-OAoHA4Aifu37NVeOLyM7pixYOZEcJe7IjMYKB6Y6w4smv7FYmjnAbhGBEbxjnX_siJX2eK3qYKAjE1oCv-6MTV78cbQHDGVPEkMdpIr6nYBvkqluyaXkcDpxMODBpoXBAaMz6uAbAj-RWkfdjMyjf4aR8NdGZmCJrwOoAKbBR7EwX1YrB_YGVNSsSC8L0bP7FV1W9skerLiVHPVs6HVV-0GP0_q744FF1PlVMl6t5u7VfmDafPL-v-btjYD2B4Mpjtlq68Ag395lqDC6nBS6l1p14HPG-JuW-IvC-2K6nNstk9PB7qbeboWmDoB9PQd9y6x1pmI00OLd3HOgmsBdZKe7jOsRFFuDV6ba5nJNK1LNBKomPfaSyNmpHnzaymCjaxekI_VgweIMLW4pp3-fA-MMJJA0tUsLGnfN1bPtbuhsBGi52lEqnpCGpSh-080DjSs_IxLwt70ki64VnxtH6ufqwIPHyqlWcPgfohUKfCw2baTk1teTkIB4ZMKjBVmoVKtUvqqNQycVpq68ujqzXOGmvSz0dceMLVNS_UdKiFZsvdcSZN5xGH8f4Xz3t7e5xus-S7yk-RvLHPxvYsHfn9_RGvf9zeZzvyY5PuZ0nF-M1DumZS1z0T6360f3wM0Oq0u9XrQ40f4f48e2wj8zhiml0V3Vpf3CbOY-N2hnxfe3tNnuCqkxL9YC2ts19Jp5P5iO6W_Ml6fNivjH3dZ27-BOH0m1PJdw_P4dPzkur3aVvun6ZT67Sww1xdGct5Wxi5tHByq3HRiUhLvzhb7HY__v7Avt_s88aQGnq7w_0-07JHdo6r7a4Nt9r49iZ5rMMbpzYe4ky6WfS9ZiJ_4a0XyamwqifOvx-SlbndH76fz7Kv-cf2WbuZh00fBDMe5IbrPCzSwpmpm2N8N09V6i5ktFVCqbvbuIuj9zcafpOwiPnr94n_AvDvmMc= \ No newline at end of file
+PKIZ_eJylWEl34rwS3etXvH2fPu0BElh6wthBcmw8IO2wnWDLNiRh8PDrXwlId4bu70u_lyxycKBUt-reWyW-f4cf3bId8h8DL8WL7wg7Ds5b6t7tmFOcMqL5mbGDZ2vTMEzbNzTf6oxQm-ub6MXcYMPmhmHsfNtYPttLJ1WR6VtzbQ0Pt5G12Tx1D70rpcqhTkvnxpnvyzSJpbU9rbIeXs_2ZWbPhkzNT1njl6tlu0HO1j2ldjw4fLdJ1H25TvzSK7WScW1gTVB4Nh3REPfErEvcWCq2WYkT2pGBFURxFIQHq1wYWpk2swNbwimG26dKV-OlO10Y-q3T1JUI7jS0xQqraFg0nm0NmPtjyt0CkUFvKJ81OMwUGuYl4bhzyhY-MC7SJDpnkzXTPQud8jGW9nBA_TDXn7ImHlbKTELn6Nxp8bA5YNM64LCIMLducOjfYDNfn4NtdcjqqaaqgCeyCk5pMnsSdUKiUD9x29MDTerjSqkrvHTEaUeayPUFwvVD9fT87AJRKxFLxgVtupoZoupBnyeR-ODT-bXhSuL_6TZ4hEM-Qcvt-IhoMpZWyvnh9Q1BnSmkX690aZ1Mj-L0dBvv0_kZP6eroEjt6fa1ayKDKrOnT3DKm1aOJbZyG5qQa_qzKgVol3rEfXrJbpfPgxZ55eSEQ0ddcO2IjVHn8Y1KBnrKuXUiPChJQ4EPcPIQDcTEMguLgnDonEJHXuKWiHAghXLhArRm-5o2EKxmSn1Kq-mRXQp6rYszUB7XJIwk2pAG4dCSGHckzyQ1EKSjTaTSUJOxyao3ZDpCwXrWzPiVbAJynUFBEeAR0eWsac-VXc8DKTN3p8Vg9aQftWdo4W5EhkxZqLRbDFSinDXAypIqWAYq-wNJIuA7bRmk5AHnKYd_hXlxKdoVSnmhOUvyp1QZ36dNdIaNXklEwgD44PfMxhLh8Gu7BbFBPLzqSOiPhahYQgpmWuUjqBBUe4aBsoYVVLlyfh6XqV3z37XrT91CX23Xn7qF_qFdoH1LZQno3nY6yisJh5XiQXCiAEQT4EAHoY11zaBdwl2cbTDO7CvPQcK5ENKZ3ldP4JEKCuXQAQ7Bey_0VYQhElbgdyhqLyHQB0uhAyk-Cec14BY0AQp-lQD6XzXwWlT0q6oVpOQIDfwNrccIc0dUF_hdyXioJGJCIDMa0wZLXsIqYmYSuFXPyt_TGr3l9RdpPQZy9YLWAhr6N6r-snmnJSEdaBM0BLRA7LgBhy6Q8HicAFUTRyGDW0Jv31H135iK_kzVrxUVfTQLMsQNULcgopChL4GMBw-mkaA1CyHtwVFYWBf0Sj70ln3rRC6Y8h47DmOO-aygSaRQ7qig0BGzLRk3UQvGoyDPjsbYLOAN-OOI26b27CjwX2tSp03Qpgq0cY7FgElFndDHQtEkOKyT0TlYvnL3F0YWUj7Xbhb9pMM8EzWCllo3npmpiBhTBS9Hn6zqq06F_rX_SVAys25IqEP_qUpMBjIGBZtWB-41IJjM8AAMxP5z_68ig5nYfoKG_oTtq9DQ37rwKzQviWDhwBIiDR6BuwzQX3DfmlOOx4zH8FeTvLAoPbFY_AO10d-5sC-ofcTLiQI-CcYGEGC-cRJi0HwsRpsCSxRnfAN-AOLilkovVL9CGV1XnosuQmVco_emOasY10tiBhWkDj4ZVAAL2qjX2PYhOHimQmqw_d7Zyvl5MuXzur1Sl6eK3Oar4IMTuw0xNQlWvtIzxZQKIOPNGOy-pIPVwpbmgEi03kti_tGJ02aq0J8TOj6yuX4SnLgsnYezvaEPY7tgtiy2r69Y2wC1kxHpgTD950JFbwr1DlJjSSTUOjGVcAKTifKgxmEgPG-ExXLRsIaZG8Fz-XWIfJ4XF2cG6e_R1zggKByp4MTDR7YKT1z-ia5Y8WFmRLAXOyIzGKj-CCuO7NlBTZK4oGE0QsSGcc61v3Lit5mi96mCQEwN6Io_O3H9-_EGEJwRVXxJjDYyaGNsg3wVS_ZMv6eh0wsHBg20HgiNGZ_XANiRghrSfsrn1Tv8dIjPdGZmBJrwe4AKbBR7EwX1YrB_YGVDKsTC6KMbv7BVPeS2SPX1xHhgK-fTqi9ajP6fVV8ciq6nypkS9--39ivzzqe7l3V_e97YizwByLPpE4P5gMSAcGrGH2ZRv6zrLjaL-4eGxfGW9esqduPZdTZG4zi26juoV_RQTbpFXMTrIQ5RPK_LvHfP2l6vyJAncSXuQj-vQqbz-6vQVp5i6uioh4ukllFxwWw3a7_dsFFncM3RNyTWtSjUwqgzBs29vIZpWMch9vet4VMz9n0HWa1r-qG1xLpma3Jk6R12IzU-pttaoQlc_wKntbTzm--str7P4JoTqbAWK_vOCrV7dIm8Dw3rUD-sCNxax1DlqHXeXYcrHcbPr_ZG-kkEyiAQgkjHVHW3OPBba3M-ybTaQ8iSrnFm5IlBQAaIrHf3Z43om-q5qEobTVtJB_wzTVtCHfQyJe6ErBKVzTaj52ktJzfLb5v18ZmC1e32cXCI5qRZzslkMg823voBTYYq2m8GfXVblmo0dM3LjN3xqVkYCzr6sV1KyTqii1l6WDem8cKauebfL3da5hH1YX0kB3S3s8JH95Yrw_PIcQ-yjZenh4leSlL5GLTsx2EXLshhkdaPVjtbG_2tGo-Ml930B6tGP4y9h0aBP1TLYtZNb8udfW9NW0t2T1M6dLvxMdzcDMroZlOOlIexdFw6M4Ybgp-rKB-k7Y2fHb4hecCPk-Tbg_zUV3f7LNaH2_HtbLr99qwVnTxedF1u3DkvvgwjZpJr_SLQ_Uh6Zg-LZnFaqgnaNo8_3Nq_XZh-86yufGsepL68H-fDj6bFE6dcNhN0_rLDIuavLz7-Cz0ItdI= \ No newline at end of file
diff --git a/examples/pki/cms/revocation_list.json b/examples/pki/cms/revocation_list.json
index 1938d3c..766c69c 100644
--- a/examples/pki/cms/revocation_list.json
+++ b/examples/pki/cms/revocation_list.json
@@ -1 +1 @@
-{"revoked": [{"expires": "2112-08-14T17:58:48Z", "id": "d592dae66875eb63559ac18eddcf8ce4"}, {"expires": "2112-08-14T17:58:48Z", "id": "15ce05fd491b79791068ed80a9c7f5e7"}, {"expires": "2112-08-14T17:58:48Z", "id": "d592dae66875eb63559ac18eddcf8ce4"}, {"expires": "2112-08-14T17:58:48Z", "id": "15ce05fd491b79791068ed80a9c7f5e7"}]} \ No newline at end of file
+{"revoked": [{"expires": "2112-08-14T17:58:48Z", "id": "db98ed2af6c6707bec6dc6c6892789a0"}, {"expires": "2112-08-14T17:58:48Z", "id": "15ce05fd491b79791068ed80a9c7f5e7"}, {"expires": "2112-08-14T17:58:48Z", "id": "db98ed2af6c6707bec6dc6c6892789a0"}, {"expires": "2112-08-14T17:58:48Z", "id": "15ce05fd491b79791068ed80a9c7f5e7"}]} \ No newline at end of file
diff --git a/examples/pki/cms/revocation_list.pem b/examples/pki/cms/revocation_list.pem
index 1cc14fb..0e81998 100644
--- a/examples/pki/cms/revocation_list.pem
+++ b/examples/pki/cms/revocation_list.pem
@@ -1,20 +1,20 @@
-----BEGIN CMS-----
MIIDTwYJKoZIhvcNAQcCoIIDQDCCAzwCAQExCTAHBgUrDgMCGjCCAVwGCSqGSIb3
DQEHAaCCAU0EggFJeyJyZXZva2VkIjogW3siZXhwaXJlcyI6ICIyMTEyLTA4LTE0
-VDE3OjU4OjQ4WiIsICJpZCI6ICJkNTkyZGFlNjY4NzVlYjYzNTU5YWMxOGVkZGNm
-OGNlNCJ9LCB7ImV4cGlyZXMiOiAiMjExMi0wOC0xNFQxNzo1ODo0OFoiLCAiaWQi
+VDE3OjU4OjQ4WiIsICJpZCI6ICJkYjk4ZWQyYWY2YzY3MDdiZWM2ZGM2YzY4OTI3
+ODlhMCJ9LCB7ImV4cGlyZXMiOiAiMjExMi0wOC0xNFQxNzo1ODo0OFoiLCAiaWQi
OiAiMTVjZTA1ZmQ0OTFiNzk3OTEwNjhlZDgwYTljN2Y1ZTcifSwgeyJleHBpcmVz
-IjogIjIxMTItMDgtMTRUMTc6NTg6NDhaIiwgImlkIjogImQ1OTJkYWU2Njg3NWVi
-NjM1NTlhYzE4ZWRkY2Y4Y2U0In0sIHsiZXhwaXJlcyI6ICIyMTEyLTA4LTE0VDE3
+IjogIjIxMTItMDgtMTRUMTc6NTg6NDhaIiwgImlkIjogImRiOThlZDJhZjZjNjcw
+N2JlYzZkYzZjNjg5Mjc4OWEwIn0sIHsiZXhwaXJlcyI6ICIyMTEyLTA4LTE0VDE3
OjU4OjQ4WiIsICJpZCI6ICIxNWNlMDVmZDQ5MWI3OTc5MTA2OGVkODBhOWM3ZjVl
NyJ9XX0xggHKMIIBxgIBATCBpDCBnjEKMAgGA1UEBRMBNTELMAkGA1UEBhMCVVMx
CzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlTdW5ueXZhbGUxEjAQBgNVBAoTCU9wZW5T
dGFjazERMA8GA1UECxMIS2V5c3RvbmUxJTAjBgkqhkiG9w0BCQEWFmtleXN0b25l
QG9wZW5zdGFjay5vcmcxFDASBgNVBAMTC1NlbGYgU2lnbmVkAgERMAcGBSsOAwIa
-MA0GCSqGSIb3DQEBAQUABIIBAH85Ac0oByRTy22PhlLifNnwKyaSWWSklKNtGQWC
-xWFSdq+TOMwtqF3+MHGP1Yv+QMbyv1/uAB60VgPPzfgjqXRDTAq7mdVqoiq5QHPp
-/ck/lrOOab43MWr4CvK1AHjqmx59ZEa9TvbzfrtCkDJMp9nDwSxVCVpQHixuvZKx
-zzG8EiF5l6CNLBhiu9Fxy1c4J97xPHpvfbCQB/F5F7d5or4hgpZ83jkWMCfqDZXT
-C9CARaLM4vSBHo3izZ0KTf9LsqxIO+rkzykF1qIAZji+YmtbBdl9+9GBTaper03+
-19ZNE4aCdpAyjirXfUatpv707w559Sl6xR7L//rxPpeT8Hs=
+MA0GCSqGSIb3DQEBAQUABIIBAGn4kryxJudTZYMf32gKnoNHeAXRb97CoCXiTgs2
+gu/blX/fwMdrL8GLg2puYR07XBgjo56vMsD94ZIRyhcS1lFti9veQHt7Xp8kbR8l
+nbx9fsOhMxUHLRnxioieA9T1ykP8ZvYV3hYCeXkIYhPgD4lAAAmNq99ZxBRS3csE
+DP+Xz1+UYvT6Qm/NWRuj7WIjofneIB7gT6L5irsU0qtMCQeqI3dsP9GSsy4HJvBR
+BBIzQ7fEMRCGTADbk4ml+6Dx+Jm5SO80NvinzxCjO3DbkcEG1pQ3RGVEn3gyzg2a
+ssaRU4ycbYACA99K5UzCtSj8glGXFa1cnx42nSn2LbfJP1M=
-----END CMS-----
diff --git a/examples/pki/cms/revocation_list.pkiz b/examples/pki/cms/revocation_list.pkiz
index 7f091de..566d331 100644
--- a/examples/pki/cms/revocation_list.pkiz
+++ b/examples/pki/cms/revocation_list.pkiz
@@ -1 +1 @@
-PKIZ_eJx9VE2TsjgYvPMr9m5NCQI6HN5DEr6CJogEEG6IyvcIjgOBX7_qVO1etja3p6urn3RX1_Px8XzQsDD9CxH_NXwIBGOdjbGzvSW4GDIKvAzdnpinIwTmEQHP4IgBG-bBXc8JsqonHo4W8nvLxydZ0D3DBukTDEQjz03nMjlTckyGdBXWuLrlkfxdJsdiTI9Ok014jRGeCDOmHQPKjhmiEOqG7FaB4laeEpX4GyOnS9CL6NSU1VNimQ2tYoXOYRNX8UxZoMYR4a4V1olFW8G1aEORo-0Q3OA2VDKref6AlG4JSlIZnJTi6CKRU9PjdL5Jrn4TXfNW7hAo08grhTeRhVXCgJS0nugys6RzLbvMGGlVNImejzFrKrqKpYRl5dUf86fN5mLDLmvDWXj5xBXmhOEH0fMHYYeAsGxNWb6mepHicsxx27zzwK0nucyp4yhY0SqXaRSWAq2IRFlTxLOhJNGhjlexEq8CEX-J39j-_wBf-Qn_HSDmNKIN0cM20T2VRPhpKVMJA6tXeK4OCzciclKFjUAnRzseRZ7n9vbZCchzDAFDsNMR_KqMLQG5BaTAgAcCKTN2BNS_c0FQGBIuoBk4MKchBDkDNYXkgEcDxHroebYxNuwcqT-XY1KcrIAbFfB-uTeGAm1MIpUJZ8us0tk4EPD5VkacYH8Vqpl8GE5twB0GKpjXfVGXljaKEHlGZLaP5nKk4mmlNoJnvZXmt9CkDlmbcVMH_u8mwpBEm5MV58Gq-Tq1YQ3y17LMgv63C0acCgSI__T6WWsIvADAZxbA_lRBJt7gdGDTarUvml15pV_jdkr9KPLrZksflhchgUemf-4XzCXjozflBbGtvRQPC4-cpkFa_gC4FsN8v5-vedUfDzoD_aY9h_2t7FXP3nfCMquXzd1105Mik-iuoGErAbvqW65qiZFqbDjN1_sD1bpDOu1LH30eorDz7JL_DMmWC_NsfRqlqTZrRHewKH80k09Spjjahu_tbriekAeXpmpuzurtrhR5l3zKVR0RdO315MgEpCFwSHdEGXxo3-RyTsQtu2q7755jd3Gv56k2pR6DpCoXcfs4wXOjLTQLsrS73EV5IUhaQg0lRecOTFV5P16D9NENG3EzqqrmN2t-2OyWyzvfdxf2aX__Ed53yKD6vzfpb6HYfw4= \ No newline at end of file
+PKIZ_eJx9VElzszgUvPMr5p5KBbPY5vAdtGAsYokAYr0ZbIvdSXDM8usHO1Uzl6lRlQ6v1dVPr6vrvb4uB5oWYX8h6j-KV4kSgvmQ2O_XlBT3nAE3R9cFczFCYB4QcM0RcbCHIvjGgiKrWvBwsJD_ZfkkUyXsmntwXMBANoXY2efJntI4vR-VsCbVVURqX6ZxMRxju8knsiaITJSb04ED7cBNWQqxqTpVoDmVq0Ul6QmyP1P0INp1UtVaGrlTEiVKMicqxacyjaiSWvRRaw4nquTgpqDINg4IbkgbarnVLD-gpVOCklbmSEt5cJA8sp07svm6cvBVdnbX8oBAeYzcUnoSeVilHKzS1pUdvivZXKsONwdWFU2KxZDwpmJKskp5Xl78QSxjNuc9_MzbcJYec5KKjJSTG8XiRrkXUJ6vGRdrhosjKQdB2ubpB2m90uEPUbtIq7RiVT5ITLGbZE7r5S6A0GmVa05kDqSTe7L_fwMf_kn_bSAZWcQaisM2xa5OI7KMlOuUA8WxwtrBsHAiqqZV2Ehsso04lkch9u9LJuAoCAQcwU-MYFeZ7xQIC6wCE3oUMm4eKKh_68X6MKSjhGZgQ8FCCAQHNYPUI4MJEhy67t4cGn6K9J9znBaZFYxmBdxf7pWjwBjSSOfSydpVx9n0KNg-ldFIia-Eeq5696wNRpuDCor6q6hLyxhkiFwz2rW35hwzOVP0RnKtp9L8FJr0e97m4w4D_7cT5WjFmsxKRKA0XdaGNRCPZrkF_d4BAzlKFMj_5HqJNQRuAODiBbA6rf6eRvvnxNOEXlRFvHdXtj-D2MuMDbqiuOSiVyTx85Y18dtloKfvw9Y6COXzJ_HkTQxFddXXd9pjQ0uJNxW5v2p2t9K4n939bRN_buvM2zZSl43GpXcKOgb7g9eN5bU8A4Ovpvpjm96TUC0SdI5rkhQfAmsNAKBlX4aRjtDz1bw3JfzxEs-rlyC587XbvrHI-6k20ZK7S3cmcCP4-qCX330gf90ocs9fRD31H4bl95O2t-_QkyAks7u5mNRDFgc4q7W2eVnj8cVudd_ZyuxedvOIKkdd3nLTWn26qmeFZqeKaRbKUer7oxdoU54lAAHDeNeDGd38aisaK94dV3k3akrnd8ohu9gfK_pHeu4hk-F_d9Lf2fB_ww== \ No newline at end of file
diff --git a/keystoneclient/__init__.py b/keystoneclient/__init__.py
index 08545c5..96f32ab 100644
--- a/keystoneclient/__init__.py
+++ b/keystoneclient/__init__.py
@@ -27,18 +27,10 @@ Identity V2 and V3 clients can also be created directly. See
"""
+import sys
import pbr.version
-from keystoneclient import access
-from keystoneclient import client
-from keystoneclient import exceptions
-from keystoneclient import generic
-from keystoneclient import httpclient
-from keystoneclient import service_catalog
-from keystoneclient import v2_0
-from keystoneclient import v3
-
__version__ = pbr.version.VersionInfo('python-keystoneclient').version_string()
@@ -55,3 +47,32 @@ __all__ = [
'httpclient',
'service_catalog',
]
+
+
+class _LazyImporter(object):
+ def __init__(self, module):
+ self._module = module
+
+ def __getattr__(self, name):
+ # NB: this is only called until the import has been done.
+ # These submodules are part of the API without explicit importing, but
+ # expensive to load, so we load them on-demand rather than up-front.
+ lazy_submodules = [
+ 'access',
+ 'client',
+ 'exceptions',
+ 'generic',
+ 'httpclient',
+ 'service_catalog',
+ 'v2_0',
+ 'v3',
+ ]
+ # __import__ rather than importlib for Python 2.6.
+ if name in lazy_submodules:
+ __import__('keystoneclient.%s' % name)
+ return getattr(self, name)
+ # Return module attributes like __all__ etc.
+ return getattr(self._module, name)
+
+
+sys.modules[__name__] = _LazyImporter(sys.modules[__name__])
diff --git a/keystoneclient/access.py b/keystoneclient/access.py
index 8217de8..009b72e 100644
--- a/keystoneclient/access.py
+++ b/keystoneclient/access.py
@@ -400,6 +400,35 @@ class AccessInfo(dict):
"""
raise NotImplementedError()
+ @property
+ def audit_id(self):
+ """Return the audit ID if present.
+
+ :returns: str or None.
+ """
+ raise NotImplementedError()
+
+ @property
+ def audit_chain_id(self):
+ """Return the audit chain ID if present.
+
+ In the event that a token was rescoped then this ID will be the
+ :py:attr:`audit_id` of the initial token. Returns None if no value
+ present.
+
+ :returns: str or None.
+ """
+ raise NotImplementedError()
+
+ @property
+ def initial_audit_id(self):
+ """The audit ID of the initially requested token.
+
+ This is the :py:attr:`audit_chain_id` if present or the
+ :py:attr:`audit_id`.
+ """
+ return self.audit_chain_id or self.audit_id
+
class AccessInfoV2(AccessInfo):
"""An object for encapsulating a raw v2 auth token from identity
@@ -592,6 +621,20 @@ class AccessInfoV2(AccessInfo):
def is_federated(self):
return False
+ @property
+ def audit_id(self):
+ try:
+ return self['token'].get('audit_ids', [])[0]
+ except IndexError:
+ return None
+
+ @property
+ def audit_chain_id(self):
+ try:
+ return self['token'].get('audit_ids', [])[1]
+ except IndexError:
+ return None
+
class AccessInfoV3(AccessInfo):
"""An object for encapsulating a raw v3 auth token from identity
@@ -760,3 +803,17 @@ class AccessInfoV3(AccessInfo):
@property
def oauth_consumer_id(self):
return self.get('OS-OAUTH1', {}).get('consumer_id')
+
+ @property
+ def audit_id(self):
+ try:
+ return self.get('audit_ids', [])[0]
+ except IndexError:
+ return None
+
+ @property
+ def audit_chain_id(self):
+ try:
+ return self.get('audit_ids', [])[1]
+ except IndexError:
+ return None
diff --git a/keystoneclient/adapter.py b/keystoneclient/adapter.py
index e8e0a29..74399da 100644
--- a/keystoneclient/adapter.py
+++ b/keystoneclient/adapter.py
@@ -40,13 +40,16 @@ class Adapter(object):
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
"""
@utils.positional()
def __init__(self, session, service_type=None, service_name=None,
interface=None, region_name=None, endpoint_override=None,
version=None, auth=None, user_agent=None,
- connect_retries=None):
+ connect_retries=None, logger=None):
# NOTE(jamielennox): when adding new parameters to adapter please also
# add them to the adapter call in httpclient.HTTPClient.__init__
self.session = session
@@ -59,6 +62,7 @@ class Adapter(object):
self.user_agent = user_agent
self.auth = auth
self.connect_retries = connect_retries
+ self.logger = logger
def _set_endpoint_filter_kwargs(self, kwargs):
if self.service_type:
@@ -85,6 +89,8 @@ class Adapter(object):
kwargs.setdefault('user_agent', self.user_agent)
if self.connect_retries is not None:
kwargs.setdefault('connect_retries', self.connect_retries)
+ if self.logger:
+ kwargs.setdefault('logger', self.logger)
return self.session.request(url, method, **kwargs)
diff --git a/keystoneclient/auth/base.py b/keystoneclient/auth/base.py
index b63d4c7..91cf86a 100644
--- a/keystoneclient/auth/base.py
+++ b/keystoneclient/auth/base.py
@@ -78,6 +78,7 @@ class BaseAuthPlugin(object):
:return: A token to use.
:rtype: string
"""
+ return None
def get_headers(self, session, **kwargs):
"""Fetch authentication headers for message.
@@ -137,6 +138,7 @@ class BaseAuthPlugin(object):
service or None if not available.
:rtype: string
"""
+ return None
def invalidate(self):
"""Invalidate the current authentication data.
@@ -253,13 +255,11 @@ class BaseAuthPlugin(object):
:returns: An auth plugin, or None if a name is not provided.
:rtype: :py:class:`keystoneclient.auth.BaseAuthPlugin`
"""
- for opt in cls.get_options():
- val = getattr(namespace, 'os_%s' % opt.dest)
- if val is not None:
- val = opt.type(val)
- kwargs.setdefault(opt.dest, val)
- return cls.load_from_options(**kwargs)
+ def _getter(opt):
+ return getattr(namespace, 'os_%s' % opt.dest)
+
+ return cls.load_from_options_getter(_getter, **kwargs)
@classmethod
def register_conf_options(cls, conf, group):
@@ -285,10 +285,34 @@ class BaseAuthPlugin(object):
:returns: An authentication Plugin.
:rtype: :py:class:`keystoneclient.auth.BaseAuthPlugin`
"""
+
+ def _getter(opt):
+ return conf[group][opt.dest]
+
+ return cls.load_from_options_getter(_getter, **kwargs)
+
+ @classmethod
+ def load_from_options_getter(cls, getter, **kwargs):
+ """Load a plugin from a getter function 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 one value, an
+ :py:class:`oslo_config.cfg.Opt` and returns a value to load with.
+
+ :param getter: A function that returns a value for the given opt.
+ :type getter: callable
+
+ :returns: An authentication Plugin.
+ :rtype: :py:class:`keystoneclient.auth.BaseAuthPlugin`
+ """
+
plugin_opts = cls.get_options()
for opt in plugin_opts:
- val = conf[group][opt.dest]
+ val = getter(opt)
if val is not None:
val = opt.type(val)
kwargs.setdefault(opt.dest, val)
diff --git a/keystoneclient/auth/identity/base.py b/keystoneclient/auth/identity/base.py
index d8cd2a6..75c6d7f 100644
--- a/keystoneclient/auth/identity/base.py
+++ b/keystoneclient/auth/identity/base.py
@@ -34,8 +34,9 @@ def get_options():
@six.add_metaclass(abc.ABCMeta)
class BaseIdentityPlugin(base.BaseAuthPlugin):
- # we count a token as valid if it is valid for at least this many seconds
- MIN_TOKEN_LIFE_SECONDS = 1
+ # 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,
diff --git a/keystoneclient/tests/functional/test_fake.py b/keystoneclient/auth/identity/v3/__init__.py
index 3aecba2..a08f3ec 100644
--- a/keystoneclient/tests/functional/test_fake.py
+++ b/keystoneclient/auth/identity/v3/__init__.py
@@ -10,16 +10,21 @@
# License for the specific language governing permissions and limitations
# under the License.
-from keystoneclient.tests.functional import base
+from keystoneclient.auth.identity.v3.base import * # noqa
+from keystoneclient.auth.identity.v3.federated import * # noqa
+from keystoneclient.auth.identity.v3.password import * # noqa
+from keystoneclient.auth.identity.v3.token import * # noqa
-class FakeTests(base.TestCase):
+__all__ = ['Auth',
+ 'AuthConstructor',
+ 'AuthMethod',
+ 'BaseAuth',
- # NOTE(jamielennox): These are purely to have something that passes to
- # submit to the gate. After that is working this file can be removed and
- # the real tests can begin to be ported from tempest.
+ 'FederatedBaseAuth',
- def test_version(self):
- # NOTE(jamilennox): lol, 1st bug: version goes to stderr - can't test
- # value, however it tests that return value = 0 automatically.
- self.keystone('', flags='--version')
+ 'Password',
+ 'PasswordMethod',
+
+ 'Token',
+ 'TokenMethod']
diff --git a/keystoneclient/auth/identity/v3.py b/keystoneclient/auth/identity/v3/base.py
index 16ecba1..9d1f562 100644
--- a/keystoneclient/auth/identity/v3.py
+++ b/keystoneclient/auth/identity/v3/base.py
@@ -24,8 +24,11 @@ from keystoneclient import utils
_logger = logging.getLogger(__name__)
+__all__ = ['Auth', 'AuthMethod', 'AuthConstructor', 'BaseAuth']
-class Auth(base.BaseIdentityPlugin):
+
+@six.add_metaclass(abc.ABCMeta)
+class BaseAuth(base.BaseIdentityPlugin):
"""Identity V3 Authentication Plugin.
:param string auth_url: Identity service endpoint for authentication.
@@ -44,7 +47,7 @@ class Auth(base.BaseIdentityPlugin):
"""
@utils.positional()
- def __init__(self, auth_url, auth_methods,
+ def __init__(self, auth_url,
trust_id=None,
domain_id=None,
domain_name=None,
@@ -54,10 +57,8 @@ class Auth(base.BaseIdentityPlugin):
project_domain_name=None,
reauthenticate=True,
include_catalog=True):
- super(Auth, self).__init__(auth_url=auth_url,
- reauthenticate=reauthenticate)
-
- self.auth_methods = auth_methods
+ super(BaseAuth, self).__init__(auth_url=auth_url,
+ reauthenticate=reauthenticate)
self.trust_id = trust_id
self.domain_id = domain_id
self.domain_name = domain_name
@@ -72,6 +73,55 @@ class Auth(base.BaseIdentityPlugin):
"""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
+
+ @classmethod
+ def get_options(cls):
+ options = super(BaseAuth, cls).get_options()
+
+ options.extend([
+ cfg.StrOpt('domain-id', help='Domain ID to scope to'),
+ cfg.StrOpt('domain-name', help='Domain name to scope to'),
+ cfg.StrOpt('project-id', help='Project ID to scope to'),
+ cfg.StrOpt('project-name', help='Project name to scope to'),
+ cfg.StrOpt('project-domain-id',
+ help='Domain ID containing project'),
+ cfg.StrOpt('project-domain-name',
+ help='Domain name containing project'),
+ cfg.StrOpt('trust-id', help='Trust ID'),
+ ])
+
+ return options
+
+
+class Auth(BaseAuth):
+ """Identity V3 Authentication Plugin.
+
+ :param string auth_url: Identity service endpoint for authentication.
+ :param list auth_methods: A collection of methods to authenticate with.
+ :param string trust_id: Trust ID for trust scoping.
+ :param string domain_id: Domain ID for domain scoping.
+ :param string domain_name: Domain name for domain scoping.
+ :param string project_id: Project ID for project scoping.
+ :param string project_name: Project name for project scoping.
+ :param string project_domain_id: Project's domain ID for project.
+ :param string project_domain_name: Project's domain name for project.
+ :param bool reauthenticate: Allow fetching a new token if the current one
+ is going to expire. (optional) default True
+ :param bool include_catalog: Include the service catalog in the returned
+ token. (optional) default True.
+ :param bool unscoped: Force the return of an unscoped token. This will make
+ the keystone server return an unscoped token even if
+ a default_project_id is set for this user.
+ """
+
+ def __init__(self, auth_url, auth_methods, **kwargs):
+ self.unscoped = kwargs.pop('unscoped', False)
+ super(Auth, self).__init__(auth_url=auth_url, **kwargs)
+ self.auth_methods = auth_methods
+
def get_auth_ref(self, session, **kwargs):
headers = {'Accept': 'application/json'}
body = {'auth': {'identity': {}}}
@@ -92,12 +142,13 @@ class Auth(base.BaseIdentityPlugin):
mutual_exclusion = [bool(self.domain_id or self.domain_name),
bool(self.project_id or self.project_name),
- bool(self.trust_id)]
+ bool(self.trust_id),
+ bool(self.unscoped)]
if sum(mutual_exclusion) > 1:
raise exceptions.AuthorizationFailure(
_('Authentication cannot be scoped to multiple targets. Pick '
- 'one of: project, domain or trust'))
+ 'one of: project, domain, trust or unscoped'))
if self.domain_id:
body['auth']['scope'] = {'domain': {'id': self.domain_id}}
@@ -115,6 +166,8 @@ class Auth(base.BaseIdentityPlugin):
scope['project']['domain'] = {'name': self.project_domain_name}
elif self.trust_id:
body['auth']['scope'] = {'OS-TRUST:trust': {'id': self.trust_id}}
+ elif self.unscoped:
+ body['auth']['scope'] = {'unscoped': {}}
# NOTE(jamielennox): we add nocatalog here rather than in token_url
# directly as some federation plugins require the base token_url
@@ -134,24 +187,6 @@ class Auth(base.BaseIdentityPlugin):
return access.AccessInfoV3(resp.headers['X-Subject-Token'],
**resp_data)
- @classmethod
- def get_options(cls):
- options = super(Auth, cls).get_options()
-
- options.extend([
- cfg.StrOpt('domain-id', help='Domain ID to scope to'),
- cfg.StrOpt('domain-name', help='Domain name to scope to'),
- cfg.StrOpt('project-id', help='Project ID to scope to'),
- cfg.StrOpt('project-name', help='Project name to scope to'),
- cfg.StrOpt('project-domain-id',
- help='Domain ID containing project'),
- cfg.StrOpt('project-domain-name',
- help='Domain name containing project'),
- cfg.StrOpt('trust-id', help='Trust ID'),
- ])
-
- return options
-
@six.add_metaclass(abc.ABCMeta)
class AuthMethod(object):
@@ -213,120 +248,3 @@ class AuthConstructor(Auth):
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)
-
-
-class PasswordMethod(AuthMethod):
- """Construct a User/Password based authentication method.
-
- :param string password: Password for authentication.
- :param string username: Username for authentication.
- :param string user_id: User ID for authentication.
- :param string user_domain_id: User's domain ID for authentication.
- :param string user_domain_name: User's domain name for authentication.
- """
-
- _method_parameters = ['user_id',
- 'username',
- 'user_domain_id',
- 'user_domain_name',
- 'password']
-
- def get_auth_data(self, session, auth, headers, **kwargs):
- user = {'password': self.password}
-
- if self.user_id:
- user['id'] = self.user_id
- elif self.username:
- user['name'] = self.username
-
- if self.user_domain_id:
- user['domain'] = {'id': self.user_domain_id}
- elif self.user_domain_name:
- user['domain'] = {'name': self.user_domain_name}
-
- return 'password', {'user': user}
-
-
-class Password(AuthConstructor):
- """A plugin for authenticating with a username and password.
-
- :param string auth_url: Identity service endpoint for authentication.
- :param string password: Password for authentication.
- :param string username: Username for authentication.
- :param string user_id: User ID for authentication.
- :param string user_domain_id: User's domain ID for authentication.
- :param string user_domain_name: User's domain name for authentication.
- :param string trust_id: Trust ID for trust scoping.
- :param string domain_id: Domain ID for domain scoping.
- :param string domain_name: Domain name for domain scoping.
- :param string project_id: Project ID for project scoping.
- :param string project_name: Project name for project scoping.
- :param string project_domain_id: Project's domain ID for project.
- :param string project_domain_name: Project's domain name for project.
- :param bool reauthenticate: Allow fetching a new token if the current one
- is going to expire. (optional) default True
- """
-
- _auth_method_class = PasswordMethod
-
- @classmethod
- def get_options(cls):
- options = super(Password, cls).get_options()
-
- options.extend([
- cfg.StrOpt('user-id', help='User ID'),
- cfg.StrOpt('user-name', dest='username', help='Username',
- deprecated_name='username'),
- cfg.StrOpt('user-domain-id', help="User's domain id"),
- cfg.StrOpt('user-domain-name', help="User's domain name"),
- cfg.StrOpt('password', secret=True, help="User's password"),
- ])
-
- return options
-
-
-class TokenMethod(AuthMethod):
- """Construct an Auth plugin to fetch a token from a token.
-
- :param string token: Token for authentication.
- """
-
- _method_parameters = ['token']
-
- def get_auth_data(self, session, auth, headers, **kwargs):
- headers['X-Auth-Token'] = self.token
- return 'token', {'id': self.token}
-
-
-class Token(AuthConstructor):
- """A plugin for authenticating with an existing Token.
-
- :param string auth_url: Identity service endpoint for authentication.
- :param string token: Token for authentication.
- :param string trust_id: Trust ID for trust scoping.
- :param string domain_id: Domain ID for domain scoping.
- :param string domain_name: Domain name for domain scoping.
- :param string project_id: Project ID for project scoping.
- :param string project_name: Project name for project scoping.
- :param string project_domain_id: Project's domain ID for project.
- :param string project_domain_name: Project's domain name for project.
- :param bool reauthenticate: Allow fetching a new token if the current one
- is going to expire. (optional) default True
- """
-
- _auth_method_class = TokenMethod
-
- def __init__(self, auth_url, token, **kwargs):
- super(Token, self).__init__(auth_url, token=token, **kwargs)
-
- @classmethod
- def get_options(cls):
- options = super(Token, cls).get_options()
-
- options.extend([
- cfg.StrOpt('token',
- secret=True,
- help='Token to authenticate with'),
- ])
-
- return options
diff --git a/keystoneclient/auth/identity/v3/federated.py b/keystoneclient/auth/identity/v3/federated.py
new file mode 100644
index 0000000..db7ad2b
--- /dev/null
+++ b/keystoneclient/auth/identity/v3/federated.py
@@ -0,0 +1,111 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import abc
+
+from oslo_config import cfg
+import six
+
+from keystoneclient.auth.identity.v3 import base
+from keystoneclient.auth.identity.v3 import token
+
+__all__ = ['FederatedBaseAuth']
+
+
+@six.add_metaclass(abc.ABCMeta)
+class FederatedBaseAuth(base.BaseAuth):
+
+ rescoping_plugin = token.Token
+
+ def __init__(self, auth_url, identity_provider, 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
+
+ """
+ super(FederatedBaseAuth, self).__init__(auth_url=auth_url, **kwargs)
+ self.identity_provider = identity_provider
+ self.protocol = protocol
+
+ @classmethod
+ def get_options(cls):
+ options = super(FederatedBaseAuth, cls).get_options()
+
+ options.extend([
+ cfg.StrOpt('identity-provider',
+ help="Identity Provider's name"),
+ cfg.StrOpt('protocol',
+ help='Protocol for federated plugin'),
+ ])
+
+ return options
+
+ @property
+ def federated_token_url(self):
+ """Full URL where authorization data is sent."""
+ values = {
+ 'host': self.auth_url.rstrip('/'),
+ 'identity_provider': self.identity_provider,
+ 'protocol': self.protocol
+ }
+ url = ("%(host)s/OS-FEDERATION/identity_providers/"
+ "%(identity_provider)s/protocols/%(protocol)s/auth")
+ url = url % values
+
+ return url
+
+ def _get_scoping_data(self):
+ return {'trust_id': self.trust_id,
+ 'domain_id': self.domain_id,
+ 'domain_name': self.domain_name,
+ 'project_id': self.project_id,
+ 'project_name': self.project_name,
+ 'project_domain_id': self.project_domain_id,
+ 'project_domain_name': self.project_domain_name}
+
+ def get_auth_ref(self, session, **kwargs):
+ """Authenticate retrieve token information.
+
+ This is a multi-step process where a client does federated authn
+ receives an unscoped token.
+
+ If an unscoped token is successfully received and scoping information
+ is present then the token is rescoped to that target.
+
+ :param session: a session object to send out HTTP requests.
+ :type session: keystoneclient.session.Session
+
+ :returns: a token data representation
+ :rtype: :py:class:`keystoneclient.access.AccessInfo`
+
+ """
+ auth_ref = self.get_unscoped_auth_ref(session)
+ scoping = self._get_scoping_data()
+
+ if any(scoping.values()):
+ token_plugin = self.rescoping_plugin(self.auth_url,
+ token=auth_ref.auth_token,
+ **scoping)
+
+ auth_ref = token_plugin.get_auth_ref(session)
+
+ return auth_ref
+
+ @abc.abstractmethod
+ def get_unscoped_auth_ref(self, session, **kwargs):
+ """Fetch unscoped federated token."""
diff --git a/keystoneclient/auth/identity/v3/password.py b/keystoneclient/auth/identity/v3/password.py
new file mode 100644
index 0000000..7e432fa
--- /dev/null
+++ b/keystoneclient/auth/identity/v3/password.py
@@ -0,0 +1,88 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_config import cfg
+
+from keystoneclient.auth.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}
+
+
+class Password(base.AuthConstructor):
+ """A plugin for authenticating with a username and password.
+
+ :param string auth_url: Identity service endpoint for authentication.
+ :param string password: Password for authentication.
+ :param string username: Username for authentication.
+ :param string user_id: User ID for authentication.
+ :param string user_domain_id: User's domain ID for authentication.
+ :param string user_domain_name: User's domain name for authentication.
+ :param string trust_id: Trust ID for trust scoping.
+ :param string domain_id: Domain ID for domain scoping.
+ :param string domain_name: Domain name for domain scoping.
+ :param string project_id: Project ID for project scoping.
+ :param string project_name: Project name for project scoping.
+ :param string project_domain_id: Project's domain ID for project.
+ :param string project_domain_name: Project's domain name for project.
+ :param bool reauthenticate: Allow fetching a new token if the current one
+ is going to expire. (optional) default True
+ """
+
+ _auth_method_class = PasswordMethod
+
+ @classmethod
+ def get_options(cls):
+ options = super(Password, cls).get_options()
+
+ options.extend([
+ cfg.StrOpt('user-id', help='User ID'),
+ cfg.StrOpt('user-name', dest='username', help='Username',
+ deprecated_name='username'),
+ cfg.StrOpt('user-domain-id', help="User's domain id"),
+ cfg.StrOpt('user-domain-name', help="User's domain name"),
+ cfg.StrOpt('password', secret=True, help="User's password"),
+ ])
+
+ return options
diff --git a/keystoneclient/auth/identity/v3/token.py b/keystoneclient/auth/identity/v3/token.py
new file mode 100644
index 0000000..d92d3fc
--- /dev/null
+++ b/keystoneclient/auth/identity/v3/token.py
@@ -0,0 +1,65 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_config import cfg
+
+from keystoneclient.auth.identity.v3 import base
+
+
+__all__ = ['TokenMethod', 'Token']
+
+
+class TokenMethod(base.AuthMethod):
+ """Construct an Auth plugin to fetch a token from a token.
+
+ :param string token: Token for authentication.
+ """
+
+ _method_parameters = ['token']
+
+ def get_auth_data(self, session, auth, headers, **kwargs):
+ headers['X-Auth-Token'] = self.token
+ return 'token', {'id': self.token}
+
+
+class Token(base.AuthConstructor):
+ """A plugin for authenticating with an existing Token.
+
+ :param string auth_url: Identity service endpoint for authentication.
+ :param string token: Token for authentication.
+ :param string trust_id: Trust ID for trust scoping.
+ :param string domain_id: Domain ID for domain scoping.
+ :param string domain_name: Domain name for domain scoping.
+ :param string project_id: Project ID for project scoping.
+ :param string project_name: Project name for project scoping.
+ :param string project_domain_id: Project's domain ID for project.
+ :param string project_domain_name: Project's domain name for project.
+ :param bool reauthenticate: Allow fetching a new token if the current one
+ is going to expire. (optional) default True
+ """
+
+ _auth_method_class = TokenMethod
+
+ def __init__(self, auth_url, token, **kwargs):
+ super(Token, self).__init__(auth_url, token=token, **kwargs)
+
+ @classmethod
+ def get_options(cls):
+ options = super(Token, cls).get_options()
+
+ options.extend([
+ cfg.StrOpt('token',
+ secret=True,
+ help='Token to authenticate with'),
+ ])
+
+ return options
diff --git a/keystoneclient/base.py b/keystoneclient/base.py
index 6f0e294..025362b 100644
--- a/keystoneclient/base.py
+++ b/keystoneclient/base.py
@@ -345,6 +345,15 @@ class CrudManager(Manager):
def _build_query(self, params):
return '?%s' % urllib.parse.urlencode(params) if params else ''
+ def build_key_only_query(self, params_list):
+ """Builds a query that does not include values, just keys.
+
+ The Identity API has some calls that define queries without values,
+ this can not be accomplished by using urllib.parse.urlencode(). This
+ method builds a query using only the keys.
+ """
+ return '?%s' % '&'.join(params_list) if params_list else ''
+
@filter_kwargs
def list(self, fallback_to_auth=False, **kwargs):
url = self.build_url(dict_args_in_out=kwargs)
diff --git a/keystoneclient/common/cms.py b/keystoneclient/common/cms.py
index b206d7b..8664de4 100644
--- a/keystoneclient/common/cms.py
+++ b/keystoneclient/common/cms.py
@@ -38,6 +38,7 @@ PKI_ASN1_PREFIX = 'MII'
PKIZ_PREFIX = 'PKIZ_'
PKIZ_CMS_FORM = 'DER'
PKI_ASN1_FORM = 'PEM'
+DEFAULT_TOKEN_DIGEST_ALGORITHM = 'sha256'
# The openssl cms command exits with these status codes.
@@ -167,8 +168,8 @@ def cms_verify(formatted, signing_cert_file_name, ca_file_name,
# You can get more from
# http://www.openssl.org/docs/apps/cms.html#EXIT_CODES
#
- # $ openssl cms -verify -certfile not_exist_file -CAfile \
- # not_exist_file -inform PEM -nosmimecap -nodetach \
+ # $ openssl cms -verify -certfile not_exist_file -CAfile
+ # not_exist_file -inform PEM -nosmimecap -nodetach
# -nocerts -noattr
# Error opening certificate file not_exist_file
#
@@ -198,11 +199,13 @@ def is_pkiz(token_text):
def pkiz_sign(text,
signing_cert_file_name,
signing_key_file_name,
- compression_level=6):
+ compression_level=6,
+ message_digest=DEFAULT_TOKEN_DIGEST_ALGORITHM):
signed = cms_sign_data(text,
signing_cert_file_name,
signing_key_file_name,
- PKIZ_CMS_FORM)
+ PKIZ_CMS_FORM,
+ message_digest=message_digest)
compressed = zlib.compress(signed, compression_level)
encoded = PKIZ_PREFIX + base64.urlsafe_b64encode(
@@ -297,13 +300,15 @@ def is_ans1_token(token):
return is_asn1_token(token)
-def cms_sign_text(data_to_sign, signing_cert_file_name, signing_key_file_name):
+def cms_sign_text(data_to_sign, signing_cert_file_name, signing_key_file_name,
+ message_digest=DEFAULT_TOKEN_DIGEST_ALGORITHM):
return cms_sign_data(data_to_sign, signing_cert_file_name,
- signing_key_file_name)
+ signing_key_file_name, message_digest=message_digest)
def cms_sign_data(data_to_sign, signing_cert_file_name, signing_key_file_name,
- outform=PKI_ASN1_FORM):
+ outform=PKI_ASN1_FORM,
+ message_digest=DEFAULT_TOKEN_DIGEST_ALGORITHM):
"""Uses OpenSSL to sign a document.
Produces a Base64 encoding of a DER formatted CMS Document
@@ -316,7 +321,7 @@ def cms_sign_data(data_to_sign, signing_cert_file_name, signing_key_file_name,
the data
:param outform: Format for the signed document PKIZ_CMS_FORM or
PKI_ASN1_FORM
-
+ :param message_digest: Digest algorithm to use when signing or resigning
"""
_ensure_subprocess()
@@ -330,7 +335,7 @@ def cms_sign_data(data_to_sign, signing_cert_file_name, signing_key_file_name,
'-outform', 'PEM',
'-nosmimecap', '-nodetach',
'-nocerts', '-noattr',
- '-md', 'sha256', ],
+ '-md', message_digest, ],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
@@ -353,8 +358,10 @@ def cms_sign_data(data_to_sign, signing_cert_file_name, signing_key_file_name,
return output
-def cms_sign_token(text, signing_cert_file_name, signing_key_file_name):
- output = cms_sign_data(text, signing_cert_file_name, signing_key_file_name)
+def cms_sign_token(text, signing_cert_file_name, signing_key_file_name,
+ message_digest=DEFAULT_TOKEN_DIGEST_ALGORITHM):
+ output = cms_sign_data(text, signing_cert_file_name, signing_key_file_name,
+ message_digest=message_digest)
return cms_to_token(output)
diff --git a/keystoneclient/exceptions.py b/keystoneclient/exceptions.py
index a76aa32..0150bf5 100644
--- a/keystoneclient/exceptions.py
+++ b/keystoneclient/exceptions.py
@@ -21,6 +21,8 @@ Exception definitions.
.. py:exception:: HttpError
+.. py:exception:: ValidationError
+
.. py:exception:: Unauthorized
"""
diff --git a/keystoneclient/fixture/v2.py b/keystoneclient/fixture/v2.py
index cd4207b..bbac2dc 100644
--- a/keystoneclient/fixture/v2.py
+++ b/keystoneclient/fixture/v2.py
@@ -43,12 +43,14 @@ class Token(dict):
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):
+ user_name=None, trust_id=None, trustee_user_id=None,
+ audit_id=None, audit_chain_id=None):
super(Token, self).__init__()
self.token_id = token_id or uuid.uuid4().hex
self.user_id = user_id or uuid.uuid4().hex
self.user_name = user_name or uuid.uuid4().hex
+ self.audit_id = audit_id or uuid.uuid4().hex
if not issued:
issued = timeutils.utcnow() - datetime.timedelta(minutes=2)
@@ -76,6 +78,9 @@ class Token(dict):
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', {})
@@ -166,7 +171,7 @@ class Token(dict):
@property
def trust_id(self):
- return self.root.setdefault('trust', {})['id']
+ return self.root.setdefault('trust', {}).get('id')
@trust_id.setter
def trust_id(self, value):
@@ -180,6 +185,30 @@ class Token(dict):
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')
diff --git a/keystoneclient/fixture/v3.py b/keystoneclient/fixture/v3.py
index b4cd0df..646e46d 100644
--- a/keystoneclient/fixture/v3.py
+++ b/keystoneclient/fixture/v3.py
@@ -28,7 +28,8 @@ class _Service(dict):
def add_endpoint(self, interface, url, region=None):
data = {'interface': interface,
'url': url,
- 'region': region}
+ 'region': region,
+ 'region_id': region}
self.setdefault('endpoints', []).append(data)
return data
@@ -61,13 +62,14 @@ class Token(dict):
project_domain_name=None, domain_id=None, domain_name=None,
trust_id=None, trust_impersonation=None, trustee_user_id=None,
trustor_user_id=None, oauth_access_token_id=None,
- oauth_consumer_id=None):
+ oauth_consumer_id=None, audit_id=None, audit_chain_id=None):
super(Token, self).__init__()
self.user_id = user_id or uuid.uuid4().hex
self.user_name = user_name or uuid.uuid4().hex
self.user_domain_id = user_domain_id or uuid.uuid4().hex
self.user_domain_name = user_domain_name or uuid.uuid4().hex
+ self.audit_id = audit_id or uuid.uuid4().hex
if not methods:
methods = ['password']
@@ -112,6 +114,9 @@ class Token(dict):
self.set_oauth(access_token_id=oauth_access_token_id,
consumer_id=oauth_consumer_id)
+ if audit_chain_id:
+ self.audit_chain_id = audit_chain_id
+
@property
def root(self):
return self.setdefault('token', {})
@@ -294,6 +299,30 @@ class Token(dict):
def oauth_consumer_id(self, value):
self.root.setdefault('OS-OAUTH1', {})['consumer_id'] = value
+ @property
+ def audit_id(self):
+ try:
+ return self.root.get('audit_ids', [])[0]
+ except IndexError:
+ return None
+
+ @audit_id.setter
+ def audit_id(self, value):
+ audit_chain_id = self.audit_chain_id
+ lval = [value] if audit_chain_id else [value, audit_chain_id]
+ self.root['audit_ids'] = lval
+
+ @property
+ def audit_chain_id(self):
+ try:
+ return self.root.get('audit_ids', [])[1]
+ except IndexError:
+ return None
+
+ @audit_chain_id.setter
+ def audit_chain_id(self, value):
+ self.root['audit_ids'] = [self.audit_id, value]
+
def validate(self):
project = self.root.get('project')
domain = self.root.get('domain')
diff --git a/keystoneclient/middleware/s3_token.py b/keystoneclient/middleware/s3_token.py
index 7552893..f8d1ce0 100644
--- a/keystoneclient/middleware/s3_token.py
+++ b/keystoneclient/middleware/s3_token.py
@@ -34,6 +34,7 @@ This WSGI component:
import logging
from oslo_serialization import jsonutils
+from oslo_utils import strutils
import requests
import six
from six.moves import urllib
@@ -116,7 +117,7 @@ class S3Token(object):
self.request_uri = '%s://%s:%s' % (auth_protocol, auth_host, auth_port)
# SSL
- insecure = conf.get('insecure', False)
+ insecure = strutils.bool_from_string(conf.get('insecure', False))
cert_file = conf.get('certfile')
key_file = conf.get('keyfile')
diff --git a/keystoneclient/service_catalog.py b/keystoneclient/service_catalog.py
index 7c9085b..143a6b7 100644
--- a/keystoneclient/service_catalog.py
+++ b/keystoneclient/service_catalog.py
@@ -49,6 +49,9 @@ class ServiceCatalog(object):
# to calls made to the service_catalog. Provide appropriate warning.
return self._region_name
+ def _get_endpoint_region(self, endpoint):
+ return endpoint.get('region_id') or endpoint.get('region')
+
@abc.abstractmethod
def get_token(self):
"""Fetch token details from service catalog.
@@ -124,15 +127,16 @@ class ServiceCatalog(object):
if service_name != sn:
continue
- sc[st] = []
+ endpoints = sc.setdefault(st, [])
for endpoint in service.get('endpoints', []):
if (endpoint_type and not
self._is_endpoint_type_match(endpoint, endpoint_type)):
continue
- if region_name and region_name != endpoint.get('region'):
+ if (region_name and
+ region_name != self._get_endpoint_region(endpoint)):
continue
- sc[st].append(endpoint)
+ endpoints.append(endpoint)
return sc
diff --git a/keystoneclient/session.py b/keystoneclient/session.py
index 0bb0de2..96df8d0 100644
--- a/keystoneclient/session.py
+++ b/keystoneclient/session.py
@@ -148,8 +148,8 @@ class Session(object):
if user_agent is not None:
self.user_agent = user_agent
- @classmethod
- def process_header(cls, header):
+ @staticmethod
+ def _process_header(header):
"""Redacts the secure headers to be logged."""
secure_headers = ('authorization', 'x-auth-token',
'x-subject-token',)
@@ -162,8 +162,8 @@ class Session(object):
@utils.positional()
def _http_log_request(self, url, method=None, data=None,
- json=None, headers=None):
- if not _logger.isEnabledFor(logging.DEBUG):
+ json=None, headers=None, logger=_logger):
+ if not logger.isEnabledFor(logging.DEBUG):
# NOTE(morganfainberg): This whole debug section is expensive,
# there is no need to do the work if we're not going to emit a
# debug log.
@@ -186,18 +186,19 @@ class Session(object):
if headers:
for header in six.iteritems(headers):
string_parts.append('-H "%s: %s"'
- % Session.process_header(header))
+ % self._process_header(header))
if json:
data = jsonutils.dumps(json)
if data:
string_parts.append("-d '%s'" % data)
- _logger.debug(' '.join(string_parts))
+ logger.debug(' '.join(string_parts))
@utils.positional()
def _http_log_response(self, response=None, json=None,
- status_code=None, headers=None, text=None):
- if not _logger.isEnabledFor(logging.DEBUG):
+ status_code=None, headers=None, text=None,
+ logger=_logger):
+ if not logger.isEnabledFor(logging.DEBUG):
return
if response:
@@ -216,18 +217,19 @@ class Session(object):
string_parts.append('[%s]' % status_code)
if headers:
for header in six.iteritems(headers):
- string_parts.append('%s: %s' % Session.process_header(header))
+ string_parts.append('%s: %s' % self._process_header(header))
if text:
string_parts.append('\nRESP BODY: %s\n' % text)
- _logger.debug(' '.join(string_parts))
+ logger.debug(' '.join(string_parts))
@utils.positional(enforcement=utils.positional.WARN)
def request(self, url, method, json=None, original_ip=None,
user_agent=None, redirect=None, authenticated=None,
endpoint_filter=None, auth=None, requests_auth=None,
raise_exc=True, allow_reauth=True, log=True,
- endpoint_override=None, connect_retries=0, **kwargs):
+ endpoint_override=None, connect_retries=0, logger=_logger,
+ **kwargs):
"""Send an HTTP request with the specified characteristics.
Wrapper around `requests.Session.request` to handle tasks such as
@@ -286,6 +288,10 @@ class Session(object):
response. (optional, default True)
:param bool log: If True then log the request and response data to the
debug log. (optional, default True)
+ :param logger: The logger object to use to log request and responses.
+ If not provided the keystoneclient.session default
+ logger will be used.
+ :type logger: logging.Logger
:param kwargs: any other parameter that can be passed to
requests.Session.request (such as `headers`). Except:
'data' will be overwritten by the data in 'json' param.
@@ -361,7 +367,8 @@ class Session(object):
if log:
self._http_log_request(url, method=method,
data=kwargs.get('data'),
- headers=headers)
+ headers=headers,
+ logger=logger)
# Force disable requests redirect handling. We will manage this below.
kwargs['allow_redirects'] = False
@@ -370,7 +377,8 @@ class Session(object):
redirect = self.redirect
send = functools.partial(self._send_request,
- url, method, redirect, log, connect_retries)
+ url, method, redirect, log, logger,
+ connect_retries)
resp = send(**kwargs)
# handle getting a 401 Unauthorized response by invalidating the plugin
@@ -384,14 +392,14 @@ class Session(object):
resp = send(**kwargs)
if raise_exc and resp.status_code >= 400:
- _logger.debug('Request returned failure status: %s',
- resp.status_code)
+ logger.debug('Request returned failure status: %s',
+ resp.status_code)
raise exceptions.from_response(resp, method, url)
return resp
- def _send_request(self, url, method, redirect, log, connect_retries,
- connect_retry_delay=0.5, **kwargs):
+ def _send_request(self, url, method, redirect, log, logger,
+ connect_retries, connect_retry_delay=0.5, **kwargs):
# NOTE(jamielennox): We handle redirection manually because the
# requests lib follows some browser patterns where it will redirect
# POSTs as GETs for certain statuses which is not want we want for an
@@ -406,8 +414,9 @@ class Session(object):
try:
try:
resp = self.session.request(method, url, **kwargs)
- except requests.exceptions.SSLError:
- msg = _('SSL exception connecting to %s') % url
+ 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
@@ -419,18 +428,18 @@ class Session(object):
if connect_retries <= 0:
raise
- _logger.info(_LI('Failure: %(e)s. Retrying in %(delay).1fs.'),
- {'e': e, 'delay': connect_retry_delay})
+ logger.info(_LI('Failure: %(e)s. Retrying in %(delay).1fs.'),
+ {'e': e, 'delay': connect_retry_delay})
time.sleep(connect_retry_delay)
return self._send_request(
- url, method, redirect, log,
+ url, method, redirect, log, logger,
connect_retries=connect_retries - 1,
connect_retry_delay=connect_retry_delay * 2,
**kwargs)
if log:
- self._http_log_response(response=resp)
+ self._http_log_response(response=resp, logger=logger)
if resp.status_code in self._REDIRECT_STATUSES:
# be careful here in python True == 1 and False == 0
@@ -446,13 +455,13 @@ class Session(object):
try:
location = resp.headers['location']
except KeyError:
- _logger.warn(_LW("Failed to redirect request to %s as new "
- "location was not provided."), resp.url)
+ logger.warn(_LW("Failed to redirect request to %s as new "
+ "location was not provided."), resp.url)
else:
# NOTE(jamielennox): We don't pass through connect_retry_delay.
# This request actually worked so we can reset the delay count.
new_resp = self._send_request(
- location, method, redirect, log,
+ location, method, redirect, log, logger,
connect_retries=connect_retries,
**kwargs)
diff --git a/keystoneclient/shell.py b/keystoneclient/shell.py
index 854cafe..1221e57 100644
--- a/keystoneclient/shell.py
+++ b/keystoneclient/shell.py
@@ -14,13 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""
-Pending deprecation: Command-line interface to the OpenStack Identity API.
-
-This CLI is pending deprecation in favor of python-openstackclient. For a
-Python library, continue using python-keystoneclient.
-
-"""
+"""Command-line interface to the OpenStack Identity API."""
from __future__ import print_function
@@ -29,6 +23,7 @@ import getpass
import logging
import os
import sys
+import warnings
from oslo_utils import encodeutils
import six
@@ -60,6 +55,16 @@ def env(*vars, **kwargs):
class OpenStackIdentityShell(object):
def __init__(self, parser_class=argparse.ArgumentParser):
+
+ # Since Python 2.7, DeprecationWarning is ignored by default, enable
+ # it so that the deprecation message is displayed.
+ warnings.simplefilter('once', category=DeprecationWarning)
+ warnings.warn(
+ 'The keystone CLI is deprecated in favor of '
+ 'python-openstackclient. For a Python library, continue using '
+ 'python-keystoneclient.', DeprecationWarning)
+ # And back to normal!
+ warnings.resetwarnings()
self.parser_class = parser_class
def get_base_parser(self):
diff --git a/keystoneclient/tests/functional/base.py b/keystoneclient/tests/functional/base.py
deleted file mode 100644
index 2f8eff5..0000000
--- a/keystoneclient/tests/functional/base.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-import os
-
-from tempest_lib.cli import base
-
-
-class TestCase(base.ClientTestBase):
-
- def _get_clients(self):
- path = os.path.join(os.path.abspath('.'), '.tox/functional/bin')
- cli_dir = os.environ.get('OS_KEYSTONECLIENT_EXEC_DIR', path)
-
- return base.CLIClient(
- username=os.environ.get('OS_USERNAME'),
- password=os.environ.get('OS_PASSWORD'),
- tenant_name=os.environ.get('OS_TENANT_NAME'),
- uri=os.environ.get('OS_AUTH_URL'),
- cli_dir=cli_dir)
-
- def keystone(self, *args, **kwargs):
- return self.clients.keystone(*args, **kwargs)
diff --git a/keystoneclient/tests/functional/hooks/post_test_hook.sh b/keystoneclient/tests/functional/hooks/post_test_hook.sh
index 0a07aa7..0a07aa7 100644..100755
--- a/keystoneclient/tests/functional/hooks/post_test_hook.sh
+++ b/keystoneclient/tests/functional/hooks/post_test_hook.sh
diff --git a/keystoneclient/tests/functional/test_access.py b/keystoneclient/tests/functional/test_access.py
new file mode 100644
index 0000000..36fe60e
--- /dev/null
+++ b/keystoneclient/tests/functional/test_access.py
@@ -0,0 +1,47 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import os
+
+from keystoneclient.auth.identity import v2
+from keystoneclient import session
+from tempest_lib import base
+
+
+class TestV2AccessInfo(base.BaseTestCase):
+
+ def setUp(self):
+ super(TestV2AccessInfo, self).setUp()
+
+ self.session = session.Session()
+
+ def test_access_audit_id(self):
+ unscoped_plugin = v2.Password(auth_url=os.environ.get('OS_AUTH_URL'),
+ username=os.environ.get('OS_USERNAME'),
+ password=os.environ.get('OS_PASSWORD'))
+
+ unscoped_auth_ref = unscoped_plugin.get_access(self.session)
+
+ self.assertIsNotNone(unscoped_auth_ref.audit_id)
+ self.assertIsNone(unscoped_auth_ref.audit_chain_id)
+
+ scoped_plugin = v2.Token(auth_url=os.environ.get('OS_AUTH_URL'),
+ token=unscoped_auth_ref.auth_token,
+ tenant_name=os.environ.get('OS_TENANT_NAME'))
+
+ scoped_auth_ref = scoped_plugin.get_access(self.session)
+
+ self.assertIsNotNone(scoped_auth_ref.audit_id)
+ self.assertIsNotNone(scoped_auth_ref.audit_chain_id)
+
+ self.assertEqual(unscoped_auth_ref.audit_id,
+ scoped_auth_ref.audit_chain_id)
diff --git a/keystoneclient/tests/functional/test_cli.py b/keystoneclient/tests/functional/test_cli.py
new file mode 100644
index 0000000..8400e7c
--- /dev/null
+++ b/keystoneclient/tests/functional/test_cli.py
@@ -0,0 +1,143 @@
+
+# Copyright 2013 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import os
+import re
+
+from tempest_lib.cli import base
+from tempest_lib import exceptions
+
+
+class SimpleReadOnlyKeystoneClientTest(base.ClientTestBase):
+ """Basic, read-only tests for Keystone CLI client.
+
+ Checks return values and output of read-only commands.
+ These tests do not presume any content, nor do they create
+ their own. They only verify the structure of output if present.
+ """
+
+ def _get_clients(self):
+ path = os.path.join(os.path.abspath('.'), '.tox/functional/bin')
+ cli_dir = os.environ.get('OS_KEYSTONECLIENT_EXEC_DIR', path)
+
+ return base.CLIClient(
+ username=os.environ.get('OS_USERNAME'),
+ password=os.environ.get('OS_PASSWORD'),
+ tenant_name=os.environ.get('OS_TENANT_NAME'),
+ uri=os.environ.get('OS_AUTH_URL'),
+ cli_dir=cli_dir)
+
+ def keystone(self, *args, **kwargs):
+ return self.clients.keystone(*args, **kwargs)
+
+ def test_admin_fake_action(self):
+ self.assertRaises(exceptions.CommandFailed,
+ self.keystone,
+ 'this-does-not-exist')
+
+ def test_admin_catalog_list(self):
+ out = self.keystone('catalog')
+ catalog = self.parser.details_multiple(out, with_label=True)
+ for svc in catalog:
+ if svc.get('__label'):
+ self.assertTrue(svc['__label'].startswith('Service:'),
+ msg=('Invalid beginning of service block: '
+ '%s' % svc['__label']))
+ # check that region and publicURL exists. One might also
+ # check for adminURL and internalURL. id seems to be optional
+ # and is missing in the catalog backend
+ self.assertIn('publicURL', svc.keys())
+ self.assertIn('region', svc.keys())
+
+ def test_admin_endpoint_list(self):
+ out = self.keystone('endpoint-list')
+ endpoints = self.parser.listing(out)
+ self.assertTableStruct(endpoints, [
+ 'id', 'region', 'publicurl', 'internalurl',
+ 'adminurl', 'service_id'])
+
+ def test_admin_endpoint_service_match(self):
+ endpoints = self.parser.listing(self.keystone('endpoint-list'))
+ services = self.parser.listing(self.keystone('service-list'))
+ svc_by_id = {}
+ for svc in services:
+ svc_by_id[svc['id']] = svc
+ for endpoint in endpoints:
+ self.assertIn(endpoint['service_id'], svc_by_id)
+
+ def test_admin_role_list(self):
+ roles = self.parser.listing(self.keystone('role-list'))
+ self.assertTableStruct(roles, ['id', 'name'])
+
+ def test_admin_service_list(self):
+ services = self.parser.listing(self.keystone('service-list'))
+ self.assertTableStruct(services, ['id', 'name', 'type', 'description'])
+
+ def test_admin_tenant_list(self):
+ tenants = self.parser.listing(self.keystone('tenant-list'))
+ self.assertTableStruct(tenants, ['id', 'name', 'enabled'])
+
+ def test_admin_user_list(self):
+ users = self.parser.listing(self.keystone('user-list'))
+ self.assertTableStruct(users, [
+ 'id', 'name', 'enabled', 'email'])
+
+ def test_admin_user_role_list(self):
+ user_roles = self.parser.listing(self.keystone('user-role-list'))
+ self.assertTableStruct(user_roles, [
+ 'id', 'name', 'user_id', 'tenant_id'])
+
+ def test_admin_discover(self):
+ discovered = self.keystone('discover')
+ self.assertIn('Keystone found at http', discovered)
+ self.assertIn('supports version', discovered)
+
+ def test_admin_help(self):
+ help_text = self.keystone('help')
+ lines = help_text.split('\n')
+ self.assertFirstLineStartsWith(lines, 'usage: keystone')
+
+ commands = []
+ cmds_start = lines.index('Positional arguments:')
+ cmds_end = lines.index('Optional arguments:')
+ command_pattern = re.compile('^ {4}([a-z0-9\-\_]+)')
+ for line in lines[cmds_start:cmds_end]:
+ match = command_pattern.match(line)
+ if match:
+ commands.append(match.group(1))
+ commands = set(commands)
+ wanted_commands = set(('catalog', 'endpoint-list', 'help',
+ 'token-get', 'discover', 'bootstrap'))
+ self.assertFalse(wanted_commands - commands)
+
+ def test_admin_bashcompletion(self):
+ self.keystone('bash-completion')
+
+ def test_admin_ec2_credentials_list(self):
+ creds = self.keystone('ec2-credentials-list')
+ creds = self.parser.listing(creds)
+ self.assertTableStruct(creds, ['tenant', 'access', 'secret'])
+
+ # Optional arguments:
+
+ def test_admin_version(self):
+ self.keystone('', flags='--version')
+
+ def test_admin_debug_list(self):
+ self.keystone('catalog', flags='--debug')
+
+ def test_admin_timeout(self):
+ self.keystone('catalog', flags='--timeout %d' % 15)
diff --git a/keystoneclient/tests/unit/auth/test_identity_common.py b/keystoneclient/tests/unit/auth/test_identity_common.py
index db30bea..3bf04c7 100644
--- a/keystoneclient/tests/unit/auth/test_identity_common.py
+++ b/keystoneclient/tests/unit/auth/test_identity_common.py
@@ -113,7 +113,7 @@ class CommonIdentityTests(object):
# 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.get(self.TEST_COMPUTE_ADMIN, resps)
+ self.requests_mock.get(self.TEST_COMPUTE_ADMIN, resps)
body = 'SUCCESS'
self.stub_url('GET', ['path'], text=body)
@@ -138,7 +138,7 @@ class CommonIdentityTests(object):
# 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.get(self.TEST_COMPUTE_ADMIN, resps)
+ self.requests_mock.get(self.TEST_COMPUTE_ADMIN, resps)
body = 'SUCCESS'
self.stub_url('GET', ['path'], text=body)
@@ -419,4 +419,5 @@ class GenericAuthPluginTests(utils.TestCase):
self.assertIsNone(self.session.get_token())
self.assertEqual(self.auth.headers,
self.session.get_auth_headers())
- self.assertNotIn('X-Auth-Token', self.requests.last_request.headers)
+ self.assertNotIn('X-Auth-Token',
+ self.requests_mock.last_request.headers)
diff --git a/keystoneclient/tests/unit/auth/test_identity_v2.py b/keystoneclient/tests/unit/auth/test_identity_v2.py
index 6d432a7..4c05ee2 100644
--- a/keystoneclient/tests/unit/auth/test_identity_v2.py
+++ b/keystoneclient/tests/unit/auth/test_identity_v2.py
@@ -200,7 +200,8 @@ class V2IdentityPlugin(utils.TestCase):
resp = s.get('/path', endpoint_filter=endpoint_filter)
self.assertEqual(resp.status_code, 200)
- self.assertEqual(self.requests.last_request.url, base_url + '/path')
+ self.assertEqual(self.requests_mock.last_request.url,
+ base_url + '/path')
def test_service_url(self):
endpoint_filter = {'service_type': 'compute',
diff --git a/keystoneclient/tests/unit/auth/test_identity_v3.py b/keystoneclient/tests/unit/auth/test_identity_v3.py
index 29cbb0e..077ebf5 100644
--- a/keystoneclient/tests/unit/auth/test_identity_v3.py
+++ b/keystoneclient/tests/unit/auth/test_identity_v3.py
@@ -15,6 +15,7 @@ import uuid
from keystoneclient import access
from keystoneclient.auth.identity import v3
+from keystoneclient.auth.identity.v3 import base as v3_base
from keystoneclient import client
from keystoneclient import exceptions
from keystoneclient import fixture
@@ -384,7 +385,8 @@ class V3IdentityPlugin(utils.TestCase):
resp = s.get('/path', endpoint_filter=endpoint_filter)
self.assertEqual(resp.status_code, 200)
- self.assertEqual(self.requests.last_request.url, base_url + '/path')
+ self.assertEqual(self.requests_mock.last_request.url,
+ base_url + '/path')
def test_service_url(self):
endpoint_filter = {'service_type': 'compute',
@@ -447,7 +449,8 @@ class V3IdentityPlugin(utils.TestCase):
{'status_code': 200, 'json': self.TEST_RESPONSE_DICT,
'headers': {'X-Subject-Token': 'token2'}}]
- self.requests.post('%s/auth/tokens' % self.TEST_URL, auth_responses)
+ 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)
@@ -487,4 +490,44 @@ class V3IdentityPlugin(utils.TestCase):
auth_url = self.TEST_URL + '/auth/tokens'
self.assertEqual(auth_url, a.token_url)
self.assertEqual(auth_url + '?nocatalog',
- self.requests.last_request.url)
+ 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({}, body['auth']['scope']['unscoped'])
+
+ def test_unscoped_with_scope_data(self):
+ a = v3.Password(self.TEST_URL,
+ user_id=uuid.uuid4().hex,
+ password=uuid.uuid4().hex,
+ unscoped=True,
+ project_id=uuid.uuid4().hex)
+
+ s = session.Session()
+
+ self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s)
diff --git a/keystoneclient/tests/unit/auth/test_identity_v3_federated.py b/keystoneclient/tests/unit/auth/test_identity_v3_federated.py
new file mode 100644
index 0000000..b0fa119
--- /dev/null
+++ b/keystoneclient/tests/unit/auth/test_identity_v3_federated.py
@@ -0,0 +1,96 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import copy
+import uuid
+
+from keystoneclient import access
+from keystoneclient.auth.identity import v3
+from keystoneclient import fixture
+from keystoneclient import session
+from keystoneclient.tests.unit import utils
+
+
+class TesterFederationPlugin(v3.FederatedBaseAuth):
+
+ def get_unscoped_auth_ref(self, sess, **kwargs):
+ # This would go and talk to an idp or something
+ resp = sess.post(self.federated_token_url, authenticated=False)
+ return access.AccessInfo.factory(resp=resp, body=resp.json())
+
+
+class V3FederatedPlugin(utils.TestCase):
+
+ AUTH_URL = 'http://keystone/v3'
+
+ def setUp(self):
+ super(V3FederatedPlugin, self).setUp()
+
+ self.unscoped_token = fixture.V3Token()
+ self.unscoped_token_id = uuid.uuid4().hex
+ self.scoped_token = copy.deepcopy(self.unscoped_token)
+ self.scoped_token.set_project_scope()
+ self.scoped_token.methods.append('token')
+ self.scoped_token_id = uuid.uuid4().hex
+
+ s = self.scoped_token.add_service('compute', name='nova')
+ s.add_standard_endpoints(public='http://nova/public',
+ admin='http://nova/admin',
+ internal='http://nova/internal')
+
+ self.idp = uuid.uuid4().hex
+ self.protocol = uuid.uuid4().hex
+
+ self.token_url = ('%s/OS-FEDERATION/identity_providers/%s/protocols/%s'
+ '/auth' % (self.AUTH_URL, self.idp, self.protocol))
+
+ headers = {'X-Subject-Token': self.unscoped_token_id}
+ self.unscoped_mock = self.requests_mock.post(self.token_url,
+ json=self.unscoped_token,
+ headers=headers)
+
+ headers = {'X-Subject-Token': self.scoped_token_id}
+ auth_url = self.AUTH_URL + '/auth/tokens'
+ self.scoped_mock = self.requests_mock.post(auth_url,
+ json=self.scoped_token,
+ headers=headers)
+
+ def get_plugin(self, **kwargs):
+ kwargs.setdefault('auth_url', self.AUTH_URL)
+ kwargs.setdefault('protocol', self.protocol)
+ kwargs.setdefault('identity_provider', self.idp)
+ return TesterFederationPlugin(**kwargs)
+
+ def test_federated_url(self):
+ plugin = self.get_plugin()
+ self.assertEqual(self.token_url, plugin.federated_token_url)
+
+ def test_unscoped_behaviour(self):
+ sess = session.Session(auth=self.get_plugin())
+ self.assertEqual(self.unscoped_token_id, sess.get_token())
+
+ self.assertTrue(self.unscoped_mock.called)
+ self.assertFalse(self.scoped_mock.called)
+
+ def test_scoped_behaviour(self):
+ auth = self.get_plugin(project_id=self.scoped_token.project_id)
+ sess = session.Session(auth=auth)
+ self.assertEqual(self.scoped_token_id, sess.get_token())
+
+ self.assertTrue(self.unscoped_mock.called)
+ self.assertTrue(self.scoped_mock.called)
+
+ def test_options(self):
+ opts = [o.name for o in v3.FederatedBaseAuth.get_options()]
+
+ self.assertIn('protocol', opts)
+ self.assertIn('identity-provider', opts)
diff --git a/keystoneclient/tests/unit/auth/test_loading.py b/keystoneclient/tests/unit/auth/test_loading.py
new file mode 100644
index 0000000..f8ef3b7
--- /dev/null
+++ b/keystoneclient/tests/unit/auth/test_loading.py
@@ -0,0 +1,47 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# 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 six
+
+from keystoneclient.tests.unit.auth import utils
+
+
+class TestOtherLoading(utils.TestCase):
+
+ def test_loading_getter(self):
+
+ called_opts = []
+
+ vals = {'a-int': 44,
+ 'a-bool': False,
+ 'a-float': 99.99,
+ 'a-str': 'value'}
+
+ val = uuid.uuid4().hex
+
+ def _getter(opt):
+ called_opts.append(opt.name)
+ # return str because oslo.config should convert them back
+ return str(vals[opt.name])
+
+ p = utils.MockPlugin.load_from_options_getter(_getter, other=val)
+
+ self.assertEqual(set(vals), set(called_opts))
+
+ for k, v in six.iteritems(vals):
+ # replace - to _ because it's the dest used to create kwargs
+ self.assertEqual(v, p[k.replace('-', '_')])
+
+ # check that additional kwargs get passed through
+ self.assertEqual(val, p['other'])
diff --git a/keystoneclient/tests/unit/auth/test_password.py b/keystoneclient/tests/unit/auth/test_password.py
index c5067c0..2891d8f 100644
--- a/keystoneclient/tests/unit/auth/test_password.py
+++ b/keystoneclient/tests/unit/auth/test_password.py
@@ -15,6 +15,7 @@ import uuid
from keystoneclient.auth.identity.generic import password
from keystoneclient.auth.identity import v2
from keystoneclient.auth.identity import v3
+from keystoneclient.auth.identity.v3 import password as v3_password
from keystoneclient.tests.unit.auth import utils
@@ -61,3 +62,7 @@ class PasswordTests(utils.GenericPluginTestCase):
self.assertEqual(set(allowed_opts), set(opts))
self.assertEqual(len(allowed_opts), len(opts))
+
+ def test_symbols(self):
+ self.assertIs(v3.Password, v3_password.Password)
+ self.assertIs(v3.PasswordMethod, v3_password.PasswordMethod)
diff --git a/keystoneclient/tests/unit/auth/test_token.py b/keystoneclient/tests/unit/auth/test_token.py
index 928e2b2..ce4c1cd 100644
--- a/keystoneclient/tests/unit/auth/test_token.py
+++ b/keystoneclient/tests/unit/auth/test_token.py
@@ -15,6 +15,7 @@ import uuid
from keystoneclient.auth.identity.generic import token
from keystoneclient.auth.identity import v2
from keystoneclient.auth.identity import v3
+from keystoneclient.auth.identity.v3 import token as v3_token
from keystoneclient.tests.unit.auth import utils
@@ -45,3 +46,7 @@ class TokenTests(utils.GenericPluginTestCase):
self.assertEqual(set(allowed_opts), set(opts))
self.assertEqual(len(allowed_opts), len(opts))
+
+ def test_symbols(self):
+ self.assertIs(v3.Token, v3_token.Token)
+ self.assertIs(v3.TokenMethod, v3_token.TokenMethod)
diff --git a/keystoneclient/tests/unit/auth/test_token_endpoint.py b/keystoneclient/tests/unit/auth/test_token_endpoint.py
index 4b5f82c..b0be8f1 100644
--- a/keystoneclient/tests/unit/auth/test_token_endpoint.py
+++ b/keystoneclient/tests/unit/auth/test_token_endpoint.py
@@ -23,7 +23,7 @@ class TokenEndpointTest(utils.TestCase):
TEST_URL = 'http://server/prefix'
def test_basic_case(self):
- self.requests.get(self.TEST_URL, text='body')
+ self.requests_mock.get(self.TEST_URL, text='body')
a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN)
s = session.Session(auth=a)
diff --git a/keystoneclient/tests/unit/auth/utils.py b/keystoneclient/tests/unit/auth/utils.py
index 6580c73..87c2b62 100644
--- a/keystoneclient/tests/unit/auth/utils.py
+++ b/keystoneclient/tests/unit/auth/utils.py
@@ -128,7 +128,7 @@ class GenericPluginTestCase(utils.TestCase):
auth_ref = auth.get_auth_ref(self.session)
self.assertIsInstance(auth_ref, access.AccessInfoV3)
self.assertEqual(self.TEST_URL + 'v3/auth/tokens',
- self.requests.last_request.url)
+ self.requests_mock.last_request.url)
self.assertIsInstance(auth._plugin, self.V3_PLUGIN_CLASS)
return auth
@@ -137,7 +137,7 @@ class GenericPluginTestCase(utils.TestCase):
auth_ref = auth.get_auth_ref(self.session)
self.assertIsInstance(auth_ref, access.AccessInfoV2)
self.assertEqual(self.TEST_URL + 'v2.0/tokens',
- self.requests.last_request.url)
+ self.requests_mock.last_request.url)
self.assertIsInstance(auth._plugin, self.V2_PLUGIN_CLASS)
return auth
diff --git a/keystoneclient/tests/unit/generic/test_client.py b/keystoneclient/tests/unit/generic/test_client.py
index e56e3df..6eb6836 100644
--- a/keystoneclient/tests/unit/generic/test_client.py
+++ b/keystoneclient/tests/unit/generic/test_client.py
@@ -56,7 +56,7 @@ EXTENSION_LIST = _create_extension_list([EXTENSION_FOO, EXTENSION_BAR])
class ClientDiscoveryTests(utils.TestCase):
def test_discover_extensions_v2(self):
- self.requests.get("%s/extensions" % V2_URL, text=EXTENSION_LIST)
+ self.requests_mock.get("%s/extensions" % V2_URL, text=EXTENSION_LIST)
extensions = client.Client().discover_extensions(url=V2_URL)
self.assertIn(EXTENSION_ALIAS_FOO, extensions)
self.assertEqual(extensions[EXTENSION_ALIAS_FOO], EXTENSION_NAME_FOO)
diff --git a/keystoneclient/tests/unit/test_auth_token_middleware.py b/keystoneclient/tests/unit/test_auth_token_middleware.py
index f3b523d..32a322d 100644
--- a/keystoneclient/tests/unit/test_auth_token_middleware.py
+++ b/keystoneclient/tests/unit/test_auth_token_middleware.py
@@ -222,7 +222,7 @@ class BaseAuthTokenMiddlewareTest(testtools.TestCase):
self.response_status = None
self.response_headers = None
- self.requests = self.useFixture(mock_fixture.Fixture())
+ self.requests_mock = self.useFixture(mock_fixture.Fixture())
def set_middleware(self, expected_env=None, conf=None):
"""Configure the class ready to call the auth_token middleware.
@@ -259,10 +259,10 @@ class BaseAuthTokenMiddlewareTest(testtools.TestCase):
def assertLastPath(self, path):
if path:
- parts = urlparse.urlparse(self.requests.last_request.url)
+ parts = urlparse.urlparse(self.requests_mock.last_request.url)
self.assertEqual(path, parts.path)
else:
- self.assertIsNone(self.requests.last_request)
+ self.assertIsNone(self.requests_mock.last_request)
class MultiStepAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
@@ -275,17 +275,19 @@ class MultiStepAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
# Get a token, then try to retrieve revocation list and get a 401.
# Get a new token, try to retrieve revocation list and return 200.
- self.requests.post("%s/v2.0/tokens" % BASE_URI, text=FAKE_ADMIN_TOKEN)
+ self.requests_mock.post("%s/v2.0/tokens" % BASE_URI,
+ text=FAKE_ADMIN_TOKEN)
text = self.examples.SIGNED_REVOCATION_LIST
- self.requests.get("%s/v2.0/tokens/revoked" % BASE_URI,
- response_list=[{'status_code': 401}, {'text': text}])
+ self.requests_mock.get("%s/v2.0/tokens/revoked" % BASE_URI,
+ response_list=[{'status_code': 401},
+ {'text': text}])
fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list())
self.assertEqual(fetched_list, self.examples.REVOCATION_LIST)
# Check that 4 requests have been made
- self.assertEqual(len(self.requests.request_history), 4)
+ self.assertEqual(len(self.requests_mock.request_history), 4)
class DiabloAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
@@ -306,17 +308,18 @@ class DiabloAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
super(DiabloAuthTokenMiddlewareTest, self).setUp(
expected_env=expected_env)
- self.requests.get("%s/" % BASE_URI,
- text=VERSION_LIST_v2,
- status_code=300)
+ self.requests_mock.get("%s/" % BASE_URI,
+ text=VERSION_LIST_v2,
+ status_code=300)
- self.requests.post("%s/v2.0/tokens" % BASE_URI, text=FAKE_ADMIN_TOKEN)
+ self.requests_mock.post("%s/v2.0/tokens" % BASE_URI,
+ text=FAKE_ADMIN_TOKEN)
self.token_id = self.examples.VALID_DIABLO_TOKEN
token_response = self.examples.JSON_TOKEN_RESPONSES[self.token_id]
url = '%s/v2.0/tokens/%s' % (BASE_URI, self.token_id)
- self.requests.get(url, text=token_response)
+ self.requests_mock.get(url, text=token_response)
self.set_middleware()
@@ -871,7 +874,7 @@ class CommonAuthTokenMiddlewareTest(object):
self.assertEqual(self.middleware.token_revocation_list, in_memory_list)
def test_invalid_revocation_list_raises_service_error(self):
- self.requests.get('%s/v2.0/tokens/revoked' % BASE_URI, text='{}')
+ self.requests_mock.get('%s/v2.0/tokens/revoked' % BASE_URI, text='{}')
self.assertRaises(auth_token.ServiceError,
self.middleware.fetch_revocation_list)
@@ -886,7 +889,7 @@ class CommonAuthTokenMiddlewareTest(object):
# remember because we are testing the middleware we stub the connection
# to the keystone server, but this is not what gets returned
invalid_uri = "%s/v2.0/tokens/invalid-token" % BASE_URI
- self.requests.get(invalid_uri, text="", status_code=404)
+ self.requests_mock.get(invalid_uri, text="", status_code=404)
req = webob.Request.blank('/')
req.headers['X-Auth-Token'] = 'invalid-token'
@@ -974,7 +977,7 @@ class CommonAuthTokenMiddlewareTest(object):
def test_memcache_set_invalid_uuid(self):
invalid_uri = "%s/v2.0/tokens/invalid-token" % BASE_URI
- self.requests.get(invalid_uri, status_code=404)
+ self.requests_mock.get(invalid_uri, status_code=404)
req = webob.Request.blank('/')
token = 'invalid-token'
@@ -1267,10 +1270,10 @@ class V2CertDownloadMiddlewareTest(BaseAuthTokenMiddlewareTest,
def test_request_no_token_dummy(self):
cms._ensure_subprocess()
- self.requests.get("%s%s" % (BASE_URI, self.ca_path),
- status_code=404)
+ self.requests_mock.get("%s%s" % (BASE_URI, self.ca_path),
+ status_code=404)
url = "%s%s" % (BASE_URI, self.signing_path)
- self.requests.get(url, status_code=404)
+ self.requests_mock.get(url, status_code=404)
self.assertRaises(exceptions.CertificateConfigError,
self.middleware.verify_signed_token,
self.examples.SIGNED_TOKEN_SCOPED,
@@ -1279,7 +1282,7 @@ class V2CertDownloadMiddlewareTest(BaseAuthTokenMiddlewareTest,
def test_fetch_signing_cert(self):
data = 'FAKE CERT'
url = '%s%s' % (BASE_URI, self.signing_path)
- self.requests.get(url, text=data)
+ self.requests_mock.get(url, text=data)
self.middleware.fetch_signing_cert()
with open(self.middleware.signing_cert_file_name, 'r') as f:
@@ -1289,7 +1292,7 @@ class V2CertDownloadMiddlewareTest(BaseAuthTokenMiddlewareTest,
def test_fetch_signing_ca(self):
data = 'FAKE CA'
- self.requests.get("%s%s" % (BASE_URI, self.ca_path), text=data)
+ self.requests_mock.get("%s%s" % (BASE_URI, self.ca_path), text=data)
self.middleware.fetch_ca_cert()
with open(self.middleware.signing_ca_file_name, 'r') as f:
@@ -1304,10 +1307,10 @@ class V2CertDownloadMiddlewareTest(BaseAuthTokenMiddlewareTest,
self.conf['auth_port'] = 1234
self.conf['auth_admin_prefix'] = '/newadmin/'
- self.requests.get("%s/newadmin%s" % (BASE_HOST, self.ca_path),
- text='FAKECA')
+ self.requests_mock.get("%s/newadmin%s" % (BASE_HOST, self.ca_path),
+ text='FAKECA')
url = "%s/newadmin%s" % (BASE_HOST, self.signing_path)
- self.requests.get(url, text='FAKECERT')
+ self.requests_mock.get(url, text='FAKECERT')
self.set_middleware(conf=self.conf)
@@ -1326,9 +1329,10 @@ class V2CertDownloadMiddlewareTest(BaseAuthTokenMiddlewareTest,
self.conf['auth_port'] = 1234
self.conf['auth_admin_prefix'] = ''
- self.requests.get("%s%s" % (BASE_HOST, self.ca_path), text='FAKECA')
- self.requests.get("%s%s" % (BASE_HOST, self.signing_path),
- text='FAKECERT')
+ self.requests_mock.get("%s%s" % (BASE_HOST, self.ca_path),
+ text='FAKECA')
+ self.requests_mock.get("%s%s" % (BASE_HOST, self.signing_path),
+ text='FAKECERT')
self.set_middleware(conf=self.conf)
@@ -1400,15 +1404,15 @@ class v2AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
self.examples.REVOKED_TOKEN_HASH_SHA256,
}
- self.requests.get("%s/" % BASE_URI,
- text=VERSION_LIST_v2,
- status_code=300)
+ self.requests_mock.get("%s/" % BASE_URI,
+ text=VERSION_LIST_v2,
+ status_code=300)
- self.requests.post("%s/v2.0/tokens" % BASE_URI,
- text=FAKE_ADMIN_TOKEN)
+ self.requests_mock.post("%s/v2.0/tokens" % BASE_URI,
+ text=FAKE_ADMIN_TOKEN)
- self.requests.get("%s/v2.0/tokens/revoked" % BASE_URI,
- text=self.examples.SIGNED_REVOCATION_LIST)
+ self.requests_mock.get("%s/v2.0/tokens/revoked" % BASE_URI,
+ text=self.examples.SIGNED_REVOCATION_LIST)
for token in (self.examples.UUID_TOKEN_DEFAULT,
self.examples.UUID_TOKEN_UNSCOPED,
@@ -1418,11 +1422,11 @@ class v2AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
self.examples.SIGNED_TOKEN_SCOPED_KEY,
self.examples.SIGNED_TOKEN_SCOPED_PKIZ_KEY,):
text = self.examples.JSON_TOKEN_RESPONSES[token]
- self.requests.get('%s/v2.0/tokens/%s' % (BASE_URI, token),
- text=text)
+ self.requests_mock.get('%s/v2.0/tokens/%s' % (BASE_URI, token),
+ text=text)
- self.requests.get('%s/v2.0/tokens/%s' % (BASE_URI, ERROR_TOKEN),
- text=network_error_response)
+ self.requests_mock.get('%s/v2.0/tokens/%s' % (BASE_URI, ERROR_TOKEN),
+ text=network_error_response)
self.set_middleware()
@@ -1498,16 +1502,17 @@ class CrossVersionAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
'auth_version': 'v2.0'
}
- self.requests.get('%s/' % BASE_URI,
- text=VERSION_LIST_v3,
- status_code=300)
+ self.requests_mock.get('%s/' % BASE_URI,
+ text=VERSION_LIST_v3,
+ status_code=300)
- self.requests.post('%s/v2.0/tokens' % BASE_URI, text=FAKE_ADMIN_TOKEN)
+ self.requests_mock.post('%s/v2.0/tokens' % BASE_URI,
+ text=FAKE_ADMIN_TOKEN)
token = self.examples.UUID_TOKEN_DEFAULT
url = '%s/v2.0/tokens/%s' % (BASE_URI, token)
response_body = self.examples.JSON_TOKEN_RESPONSES[token]
- self.requests.get(url, text=response_body)
+ self.requests_mock.get(url, text=response_body)
self.set_middleware(conf=conf)
@@ -1579,18 +1584,19 @@ class v3AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
self.examples.REVOKED_v3_PKIZ_TOKEN_HASH,
}
- self.requests.get(BASE_URI, text=VERSION_LIST_v3, status_code=300)
+ self.requests_mock.get(BASE_URI, text=VERSION_LIST_v3, status_code=300)
# TODO(jamielennox): auth_token middleware uses a v2 admin token
# regardless of the auth_version that is set.
- self.requests.post('%s/v2.0/tokens' % BASE_URI, text=FAKE_ADMIN_TOKEN)
+ self.requests_mock.post('%s/v2.0/tokens' % BASE_URI,
+ text=FAKE_ADMIN_TOKEN)
# TODO(jamielennox): there is no v3 revocation url yet, it uses v2
- self.requests.get('%s/v2.0/tokens/revoked' % BASE_URI,
- text=self.examples.SIGNED_REVOCATION_LIST)
+ self.requests_mock.get('%s/v2.0/tokens/revoked' % BASE_URI,
+ text=self.examples.SIGNED_REVOCATION_LIST)
- self.requests.get('%s/v3/auth/tokens' % BASE_URI,
- text=self.token_response)
+ self.requests_mock.get('%s/v3/auth/tokens' % BASE_URI,
+ text=self.token_response)
self.set_middleware()
diff --git a/keystoneclient/tests/unit/test_discovery.py b/keystoneclient/tests/unit/test_discovery.py
index 6c208a3..76aaf03 100644
--- a/keystoneclient/tests/unit/test_discovery.py
+++ b/keystoneclient/tests/unit/test_discovery.py
@@ -242,7 +242,7 @@ class AvailableVersionsTests(utils.TestCase):
for path, text in six.iteritems(examples):
url = "%s%s" % (BASE_URL, path)
- self.requests.get(url, status_code=300, text=text)
+ self.requests_mock.get(url, status_code=300, text=text)
versions = discover.available_versions(url)
for v in versions:
@@ -252,7 +252,7 @@ class AvailableVersionsTests(utils.TestCase):
matchers.Contains(n)))
def test_available_versions_individual(self):
- self.requests.get(V3_URL, status_code=200, text=V3_VERSION_ENTRY)
+ self.requests_mock.get(V3_URL, status_code=200, text=V3_VERSION_ENTRY)
versions = discover.available_versions(V3_URL)
@@ -263,7 +263,7 @@ class AvailableVersionsTests(utils.TestCase):
self.assertIn('links', v)
def test_available_keystone_data(self):
- self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+ self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
versions = discover.available_versions(BASE_URL)
self.assertEqual(2, len(versions))
@@ -278,7 +278,7 @@ class AvailableVersionsTests(utils.TestCase):
def test_available_cinder_data(self):
text = jsonutils.dumps(CINDER_EXAMPLES)
- self.requests.get(BASE_URL, status_code=300, text=text)
+ self.requests_mock.get(BASE_URL, status_code=300, text=text)
versions = discover.available_versions(BASE_URL)
self.assertEqual(2, len(versions))
@@ -294,7 +294,7 @@ class AvailableVersionsTests(utils.TestCase):
def test_available_glance_data(self):
text = jsonutils.dumps(GLANCE_EXAMPLES)
- self.requests.get(BASE_URL, status_code=200, text=text)
+ self.requests_mock.get(BASE_URL, status_code=200, text=text)
versions = discover.available_versions(BASE_URL)
self.assertEqual(5, len(versions))
@@ -311,9 +311,9 @@ class AvailableVersionsTests(utils.TestCase):
class ClientDiscoveryTests(utils.TestCase):
def assertCreatesV3(self, **kwargs):
- self.requests.post('%s/auth/tokens' % V3_URL,
- text=V3_AUTH_RESPONSE,
- headers={'X-Subject-Token': V3_TOKEN})
+ self.requests_mock.post('%s/auth/tokens' % V3_URL,
+ text=V3_AUTH_RESPONSE,
+ headers={'X-Subject-Token': V3_TOKEN})
kwargs.setdefault('username', 'foo')
kwargs.setdefault('password', 'bar')
@@ -322,7 +322,7 @@ class ClientDiscoveryTests(utils.TestCase):
return keystone
def assertCreatesV2(self, **kwargs):
- self.requests.post("%s/tokens" % V2_URL, text=V2_AUTH_RESPONSE)
+ self.requests_mock.post("%s/tokens" % V2_URL, text=V2_AUTH_RESPONSE)
kwargs.setdefault('username', 'foo')
kwargs.setdefault('password', 'bar')
@@ -345,78 +345,78 @@ class ClientDiscoveryTests(utils.TestCase):
client.Client, **kwargs)
def test_discover_v3(self):
- self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+ self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
self.assertCreatesV3(auth_url=BASE_URL)
def test_discover_v2(self):
- self.requests.get(BASE_URL, status_code=300, text=V2_VERSION_LIST)
- self.requests.post("%s/tokens" % V2_URL, text=V2_AUTH_RESPONSE)
+ self.requests_mock.get(BASE_URL, status_code=300, text=V2_VERSION_LIST)
+ self.requests_mock.post("%s/tokens" % V2_URL, text=V2_AUTH_RESPONSE)
self.assertCreatesV2(auth_url=BASE_URL)
def test_discover_endpoint_v2(self):
- self.requests.get(BASE_URL, status_code=300, text=V2_VERSION_LIST)
+ self.requests_mock.get(BASE_URL, status_code=300, text=V2_VERSION_LIST)
self.assertCreatesV2(endpoint=BASE_URL, token='fake-token')
def test_discover_endpoint_v3(self):
- self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+ self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
self.assertCreatesV3(endpoint=BASE_URL, token='fake-token')
def test_discover_invalid_major_version(self):
- self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+ self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
self.assertVersionNotAvailable(auth_url=BASE_URL, version=5)
def test_discover_200_response_fails(self):
- self.requests.get(BASE_URL, text='ok')
+ self.requests_mock.get(BASE_URL, text='ok')
self.assertDiscoveryFailure(auth_url=BASE_URL)
def test_discover_minor_greater_than_available_fails(self):
- self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+ self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
self.assertVersionNotAvailable(endpoint=BASE_URL, version=3.4)
def test_discover_individual_version_v2(self):
- self.requests.get(V2_URL, text=V2_VERSION_ENTRY)
+ self.requests_mock.get(V2_URL, text=V2_VERSION_ENTRY)
self.assertCreatesV2(auth_url=V2_URL)
def test_discover_individual_version_v3(self):
- self.requests.get(V3_URL, text=V3_VERSION_ENTRY)
+ self.requests_mock.get(V3_URL, text=V3_VERSION_ENTRY)
self.assertCreatesV3(auth_url=V3_URL)
def test_discover_individual_endpoint_v2(self):
- self.requests.get(V2_URL, text=V2_VERSION_ENTRY)
+ self.requests_mock.get(V2_URL, text=V2_VERSION_ENTRY)
self.assertCreatesV2(endpoint=V2_URL, token='fake-token')
def test_discover_individual_endpoint_v3(self):
- self.requests.get(V3_URL, text=V3_VERSION_ENTRY)
+ self.requests_mock.get(V3_URL, text=V3_VERSION_ENTRY)
self.assertCreatesV3(endpoint=V3_URL, token='fake-token')
def test_discover_fail_to_create_bad_individual_version(self):
- self.requests.get(V2_URL, text=V2_VERSION_ENTRY)
- self.requests.get(V3_URL, text=V3_VERSION_ENTRY)
+ self.requests_mock.get(V2_URL, text=V2_VERSION_ENTRY)
+ self.requests_mock.get(V3_URL, text=V3_VERSION_ENTRY)
self.assertVersionNotAvailable(auth_url=V2_URL, version=3)
self.assertVersionNotAvailable(auth_url=V3_URL, version=2)
def test_discover_unstable_versions(self):
version_list = fixture.DiscoveryList(BASE_URL, v3_status='beta')
- self.requests.get(BASE_URL, status_code=300, json=version_list)
+ self.requests_mock.get(BASE_URL, status_code=300, json=version_list)
self.assertCreatesV2(auth_url=BASE_URL)
self.assertVersionNotAvailable(auth_url=BASE_URL, version=3)
self.assertCreatesV3(auth_url=BASE_URL, unstable=True)
def test_discover_forwards_original_ip(self):
- self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+ self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
ip = '192.168.1.1'
self.assertCreatesV3(auth_url=BASE_URL, original_ip=ip)
- self.assertThat(self.requests.last_request.headers['forwarded'],
+ self.assertThat(self.requests_mock.last_request.headers['forwarded'],
matchers.Contains(ip))
def test_discover_bad_args(self):
@@ -424,7 +424,7 @@ class ClientDiscoveryTests(utils.TestCase):
client.Client)
def test_discover_bad_response(self):
- self.requests.get(BASE_URL, status_code=300, json={'FOO': 'BAR'})
+ self.requests_mock.get(BASE_URL, status_code=300, json={'FOO': 'BAR'})
self.assertDiscoveryFailure(auth_url=BASE_URL)
def test_discovery_ignore_invalid(self):
@@ -433,40 +433,40 @@ class ClientDiscoveryTests(utils.TestCase):
'media-types': V3_MEDIA_TYPES,
'status': 'stable',
'updated': UPDATED}]
- self.requests.get(BASE_URL, status_code=300,
- text=_create_version_list(resp))
+ self.requests_mock.get(BASE_URL, status_code=300,
+ text=_create_version_list(resp))
self.assertDiscoveryFailure(auth_url=BASE_URL)
def test_ignore_entry_without_links(self):
v3 = V3_VERSION.copy()
v3['links'] = []
- self.requests.get(BASE_URL, status_code=300,
- text=_create_version_list([v3, V2_VERSION]))
+ self.requests_mock.get(BASE_URL, status_code=300,
+ text=_create_version_list([v3, V2_VERSION]))
self.assertCreatesV2(auth_url=BASE_URL)
def test_ignore_entry_without_status(self):
v3 = V3_VERSION.copy()
del v3['status']
- self.requests.get(BASE_URL, status_code=300,
- text=_create_version_list([v3, V2_VERSION]))
+ self.requests_mock.get(BASE_URL, status_code=300,
+ text=_create_version_list([v3, V2_VERSION]))
self.assertCreatesV2(auth_url=BASE_URL)
def test_greater_version_than_required(self):
versions = fixture.DiscoveryList(BASE_URL, v3_id='v3.6')
- self.requests.get(BASE_URL, json=versions)
+ self.requests_mock.get(BASE_URL, json=versions)
self.assertCreatesV3(auth_url=BASE_URL, version=(3, 4))
def test_lesser_version_than_required(self):
versions = fixture.DiscoveryList(BASE_URL, v3_id='v3.4')
- self.requests.get(BASE_URL, json=versions)
+ self.requests_mock.get(BASE_URL, json=versions)
self.assertVersionNotAvailable(auth_url=BASE_URL, version=(3, 6))
def test_bad_response(self):
- self.requests.get(BASE_URL, status_code=300, text="Ugly Duckling")
+ self.requests_mock.get(BASE_URL, status_code=300, text="Ugly Duckling")
self.assertDiscoveryFailure(auth_url=BASE_URL)
def test_pass_client_arguments(self):
- self.requests.get(BASE_URL, status_code=300, text=V2_VERSION_LIST)
+ self.requests_mock.get(BASE_URL, status_code=300, text=V2_VERSION_LIST)
kwargs = {'original_ip': '100', 'use_keyring': False,
'stale_duration': 15}
@@ -477,11 +477,11 @@ class ClientDiscoveryTests(utils.TestCase):
self.assertFalse(cl.use_keyring)
def test_overriding_stored_kwargs(self):
- self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+ self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
- self.requests.post("%s/auth/tokens" % V3_URL,
- text=V3_AUTH_RESPONSE,
- headers={'X-Subject-Token': V3_TOKEN})
+ self.requests_mock.post("%s/auth/tokens" % V3_URL,
+ text=V3_AUTH_RESPONSE,
+ headers={'X-Subject-Token': V3_TOKEN})
disc = discover.Discover(auth_url=BASE_URL, debug=False,
username='foo')
@@ -494,7 +494,9 @@ class ClientDiscoveryTests(utils.TestCase):
self.assertEqual(client.password, 'bar')
def test_available_versions(self):
- self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_ENTRY)
+ self.requests_mock.get(BASE_URL,
+ status_code=300,
+ text=V3_VERSION_ENTRY)
disc = discover.Discover(auth_url=BASE_URL)
versions = disc.available_versions()
@@ -509,7 +511,7 @@ class ClientDiscoveryTests(utils.TestCase):
'updated': UPDATED}
versions = fixture.DiscoveryList()
versions.add_version(V4_VERSION)
- self.requests.get(BASE_URL, status_code=300, json=versions)
+ self.requests_mock.get(BASE_URL, status_code=300, json=versions)
disc = discover.Discover(auth_url=BASE_URL)
self.assertRaises(exceptions.DiscoveryFailure,
@@ -517,14 +519,14 @@ class ClientDiscoveryTests(utils.TestCase):
def test_discovery_fail_for_missing_v3(self):
versions = fixture.DiscoveryList(v2=True, v3=False)
- self.requests.get(BASE_URL, status_code=300, json=versions)
+ self.requests_mock.get(BASE_URL, status_code=300, json=versions)
disc = discover.Discover(auth_url=BASE_URL)
self.assertRaises(exceptions.DiscoveryFailure,
disc.create_client, version=(3, 0))
def _do_discovery_call(self, token=None, **kwargs):
- self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+ self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
if not token:
token = uuid.uuid4().hex
@@ -536,7 +538,7 @@ class ClientDiscoveryTests(utils.TestCase):
# will default to true as there is a plugin on the session
discover.Discover(s, auth_url=BASE_URL, **kwargs)
- self.assertEqual(BASE_URL, self.requests.last_request.url)
+ self.assertEqual(BASE_URL, self.requests_mock.last_request.url)
def test_setting_authenticated_true(self):
token = uuid.uuid4().hex
@@ -545,13 +547,14 @@ class ClientDiscoveryTests(utils.TestCase):
def test_setting_authenticated_false(self):
self._do_discovery_call(authenticated=False)
- self.assertNotIn('X-Auth-Token', self.requests.last_request.headers)
+ self.assertNotIn('X-Auth-Token',
+ self.requests_mock.last_request.headers)
class DiscoverQueryTests(utils.TestCase):
def test_available_keystone_data(self):
- self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+ self.requests_mock.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
disc = discover.Discover(auth_url=BASE_URL)
versions = disc.version_data()
@@ -579,7 +582,7 @@ class DiscoverQueryTests(utils.TestCase):
def test_available_cinder_data(self):
text = jsonutils.dumps(CINDER_EXAMPLES)
- self.requests.get(BASE_URL, status_code=300, text=text)
+ self.requests_mock.get(BASE_URL, status_code=300, text=text)
v1_url = "%sv1/" % BASE_URL
v2_url = "%sv2/" % BASE_URL
@@ -610,7 +613,7 @@ class DiscoverQueryTests(utils.TestCase):
def test_available_glance_data(self):
text = jsonutils.dumps(GLANCE_EXAMPLES)
- self.requests.get(BASE_URL, text=text)
+ self.requests_mock.get(BASE_URL, text=text)
v1_url = "%sv1/" % BASE_URL
v2_url = "%sv2/" % BASE_URL
@@ -659,7 +662,7 @@ class DiscoverQueryTests(utils.TestCase):
'status': status,
'updated': UPDATED}]
text = jsonutils.dumps({'versions': version_list})
- self.requests.get(BASE_URL, text=text)
+ self.requests_mock.get(BASE_URL, text=text)
disc = discover.Discover(auth_url=BASE_URL)
@@ -681,7 +684,7 @@ class DiscoverQueryTests(utils.TestCase):
'status': status,
'updated': UPDATED}]
text = jsonutils.dumps({'versions': version_list})
- self.requests.get(BASE_URL, text=text)
+ self.requests_mock.get(BASE_URL, text=text)
disc = discover.Discover(auth_url=BASE_URL)
@@ -698,7 +701,7 @@ class DiscoverQueryTests(utils.TestCase):
status = 'abcdef'
version_list = fixture.DiscoveryList(BASE_URL, v2=False,
v3_status=status)
- self.requests.get(BASE_URL, json=version_list)
+ self.requests_mock.get(BASE_URL, json=version_list)
disc = discover.Discover(auth_url=BASE_URL)
versions = disc.version_data()
@@ -727,7 +730,7 @@ class DiscoverQueryTests(utils.TestCase):
}]
text = jsonutils.dumps({'versions': version_list})
- self.requests.get(BASE_URL, text=text)
+ self.requests_mock.get(BASE_URL, text=text)
disc = discover.Discover(auth_url=BASE_URL)
diff --git a/keystoneclient/tests/unit/test_fixtures.py b/keystoneclient/tests/unit/test_fixtures.py
index 8080c82..3e41b40 100644
--- a/keystoneclient/tests/unit/test_fixtures.py
+++ b/keystoneclient/tests/unit/test_fixtures.py
@@ -35,6 +35,7 @@ class V2TokenTests(utils.TestCase):
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
@@ -48,6 +49,7 @@ class V2TokenTests(utils.TestCase):
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
@@ -233,5 +235,6 @@ class V3TokenTests(utils.TestCase):
self.assertEqual(service_type, service['type'])
for interface, url in six.iteritems(endpoints):
- endpoint = {'interface': interface, 'url': url, 'region': region}
+ endpoint = {'interface': interface, 'url': url,
+ 'region': region, 'region_id': region}
self.assertIn(endpoint, service['endpoints'])
diff --git a/keystoneclient/tests/unit/test_http.py b/keystoneclient/tests/unit/test_http.py
index 6dfceec..436c374 100644
--- a/keystoneclient/tests/unit/test_http.py
+++ b/keystoneclient/tests/unit/test_http.py
@@ -68,8 +68,8 @@ class ClientTest(utils.TestCase):
self.stub_url('GET', text=RESPONSE_BODY)
resp, body = cl.get("/hi")
- self.assertEqual(self.requests.last_request.method, 'GET')
- self.assertEqual(self.requests.last_request.url, self.TEST_URL)
+ self.assertEqual(self.requests_mock.last_request.method, 'GET')
+ self.assertEqual(self.requests_mock.last_request.url, self.TEST_URL)
self.assertRequestHeaderEqual('X-Auth-Token', 'token')
self.assertRequestHeaderEqual('User-Agent', httpclient.USER_AGENT)
@@ -108,8 +108,8 @@ class ClientTest(utils.TestCase):
self.stub_url('POST')
cl.post("/hi", body=[1, 2, 3])
- self.assertEqual(self.requests.last_request.method, 'POST')
- self.assertEqual(self.requests.last_request.body, '[1, 2, 3]')
+ self.assertEqual(self.requests_mock.last_request.method, 'POST')
+ self.assertEqual(self.requests_mock.last_request.body, '[1, 2, 3]')
self.assertRequestHeaderEqual('X-Auth-Token', 'token')
self.assertRequestHeaderEqual('Content-Type', 'application/json')
@@ -164,8 +164,8 @@ class BasicRequestTests(utils.TestCase):
if not url:
url = self.url
- self.requests.register_uri(method, url, text=response,
- status_code=status_code)
+ self.requests_mock.register_uri(method, url, text=response,
+ status_code=status_code)
return httpclient.request(url, method, **kwargs)
@@ -176,7 +176,7 @@ class BasicRequestTests(utils.TestCase):
self.request(method=method, status_code=status, response=response)
- self.assertEqual(self.requests.last_request.method, method)
+ self.assertEqual(self.requests_mock.last_request.method, method)
logger_message = self.logger_message.getvalue()
diff --git a/keystoneclient/tests/unit/test_s3_token_middleware.py b/keystoneclient/tests/unit/test_s3_token_middleware.py
index 63f9e72..dfb4406 100644
--- a/keystoneclient/tests/unit/test_s3_token_middleware.py
+++ b/keystoneclient/tests/unit/test_s3_token_middleware.py
@@ -64,7 +64,9 @@ class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase):
super(S3TokenMiddlewareTestGood, self).setUp()
self.middleware = s3_token.S3Token(FakeApp(), self.conf)
- self.requests.post(self.TEST_URL, status_code=201, json=GOOD_RESPONSE)
+ self.requests_mock.post(self.TEST_URL,
+ status_code=201,
+ json=GOOD_RESPONSE)
# Ignore the request and pass to the next middleware in the
# pipeline if no path has been specified.
@@ -98,7 +100,7 @@ class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase):
TEST_URL = 'http://%s:%d/v2.0/s3tokens' % (self.TEST_HOST,
self.TEST_PORT)
- self.requests.post(TEST_URL, status_code=201, json=GOOD_RESPONSE)
+ self.requests_mock.post(TEST_URL, status_code=201, json=GOOD_RESPONSE)
self.middleware = (
s3_token.filter_factory({'auth_protocol': 'http',
@@ -122,7 +124,7 @@ class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase):
@mock.patch.object(requests, 'post')
def test_insecure(self, MOCK_REQUEST):
self.middleware = (
- s3_token.filter_factory({'insecure': True})(FakeApp()))
+ s3_token.filter_factory({'insecure': 'True'})(FakeApp()))
text_return_value = jsonutils.dumps(GOOD_RESPONSE)
if six.PY3:
@@ -140,6 +142,28 @@ class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase):
mock_args, mock_kwargs = MOCK_REQUEST.call_args
self.assertIs(mock_kwargs['verify'], False)
+ def test_insecure_option(self):
+ # insecure is passed as a string.
+
+ # Some non-secure values.
+ true_values = ['true', 'True', '1', 'yes']
+ for val in true_values:
+ config = {'insecure': val, 'certfile': 'false_ind'}
+ middleware = s3_token.filter_factory(config)(FakeApp())
+ self.assertIs(False, middleware.verify)
+
+ # Some "secure" values, including unexpected value.
+ false_values = ['false', 'False', '0', 'no', 'someweirdvalue']
+ for val in false_values:
+ config = {'insecure': val, 'certfile': 'false_ind'}
+ middleware = s3_token.filter_factory(config)(FakeApp())
+ self.assertEqual('false_ind', middleware.verify)
+
+ # Default is secure.
+ config = {'certfile': 'false_ind'}
+ middleware = s3_token.filter_factory(config)(FakeApp())
+ self.assertIs('false_ind', middleware.verify)
+
class S3TokenMiddlewareTestBad(S3TokenMiddlewareTestBase):
def setUp(self):
@@ -151,7 +175,7 @@ class S3TokenMiddlewareTestBad(S3TokenMiddlewareTestBase):
{"message": "EC2 access key not found.",
"code": 401,
"title": "Unauthorized"}}
- self.requests.post(self.TEST_URL, status_code=403, json=ret)
+ self.requests_mock.post(self.TEST_URL, status_code=403, json=ret)
req = webob.Request.blank('/v1/AUTH_cfa/c/o')
req.headers['Authorization'] = 'access:signature'
req.headers['X-Storage-Token'] = 'token'
@@ -183,7 +207,9 @@ class S3TokenMiddlewareTestBad(S3TokenMiddlewareTestBase):
self.assertEqual(resp.status_int, s3_invalid_req.status_int)
def test_bad_reply(self):
- self.requests.post(self.TEST_URL, status_code=201, text="<badreply>")
+ self.requests_mock.post(self.TEST_URL,
+ status_code=201,
+ text="<badreply>")
req = webob.Request.blank('/v1/AUTH_cfa/c/o')
req.headers['Authorization'] = 'access:signature'
diff --git a/keystoneclient/tests/unit/test_session.py b/keystoneclient/tests/unit/test_session.py
index 1d01c3a..d521097 100644
--- a/keystoneclient/tests/unit/test_session.py
+++ b/keystoneclient/tests/unit/test_session.py
@@ -12,6 +12,7 @@
import argparse
import itertools
+import logging
import uuid
import mock
@@ -25,6 +26,7 @@ from testtools import matchers
from keystoneclient import adapter
from keystoneclient.auth import base
from keystoneclient import exceptions
+from keystoneclient.i18n import _
from keystoneclient import session as client_session
from keystoneclient.tests.unit import utils
@@ -38,7 +40,7 @@ class SessionTests(utils.TestCase):
self.stub_url('GET', text='response')
resp = session.get(self.TEST_URL)
- self.assertEqual('GET', self.requests.last_request.method)
+ self.assertEqual('GET', self.requests_mock.last_request.method)
self.assertEqual(resp.text, 'response')
self.assertTrue(resp.ok)
@@ -47,7 +49,7 @@ class SessionTests(utils.TestCase):
self.stub_url('POST', text='response')
resp = session.post(self.TEST_URL, json={'hello': 'world'})
- self.assertEqual('POST', self.requests.last_request.method)
+ self.assertEqual('POST', self.requests_mock.last_request.method)
self.assertEqual(resp.text, 'response')
self.assertTrue(resp.ok)
self.assertRequestBodyIs(json={'hello': 'world'})
@@ -57,7 +59,7 @@ class SessionTests(utils.TestCase):
self.stub_url('HEAD')
resp = session.head(self.TEST_URL)
- self.assertEqual('HEAD', self.requests.last_request.method)
+ self.assertEqual('HEAD', self.requests_mock.last_request.method)
self.assertTrue(resp.ok)
self.assertRequestBodyIs('')
@@ -66,7 +68,7 @@ class SessionTests(utils.TestCase):
self.stub_url('PUT', text='response')
resp = session.put(self.TEST_URL, json={'hello': 'world'})
- self.assertEqual('PUT', self.requests.last_request.method)
+ self.assertEqual('PUT', self.requests_mock.last_request.method)
self.assertEqual(resp.text, 'response')
self.assertTrue(resp.ok)
self.assertRequestBodyIs(json={'hello': 'world'})
@@ -76,7 +78,7 @@ class SessionTests(utils.TestCase):
self.stub_url('DELETE', text='response')
resp = session.delete(self.TEST_URL)
- self.assertEqual('DELETE', self.requests.last_request.method)
+ self.assertEqual('DELETE', self.requests_mock.last_request.method)
self.assertTrue(resp.ok)
self.assertEqual(resp.text, 'response')
@@ -85,7 +87,7 @@ class SessionTests(utils.TestCase):
self.stub_url('PATCH', text='response')
resp = session.patch(self.TEST_URL, json={'hello': 'world'})
- self.assertEqual('PATCH', self.requests.last_request.method)
+ self.assertEqual('PATCH', self.requests_mock.last_request.method)
self.assertTrue(resp.ok)
self.assertEqual(resp.text, 'response')
self.assertRequestBodyIs(json={'hello': 'world'})
@@ -202,7 +204,7 @@ class SessionTests(utils.TestCase):
m.assert_called_with(2.0)
# we count retries so there will be one initial request + 3 retries
- self.assertThat(self.requests.request_history,
+ self.assertThat(self.requests_mock.request_history,
matchers.HasLength(retries + 1))
def test_uses_tcp_keepalive_by_default(self):
@@ -218,6 +220,23 @@ class SessionTests(utils.TestCase):
client_session.Session(session=mock_session)
self.assertFalse(mock_session.mount.called)
+ def test_ssl_error_message(self):
+ error = uuid.uuid4().hex
+
+ def _ssl_error(request, context):
+ raise requests.exceptions.SSLError(error)
+
+ self.stub_url('GET', text=_ssl_error)
+ session = client_session.Session()
+
+ # The exception should contain the URL and details about the SSL error
+ msg = _('SSL exception connecting to %(url)s: %(error)s') % {
+ 'url': self.TEST_URL, 'error': error}
+ self.assertRaisesRegex(exceptions.SSLError,
+ msg,
+ session.get,
+ self.TEST_URL)
+
class RedirectTests(utils.TestCase):
@@ -234,14 +253,14 @@ class RedirectTests(utils.TestCase):
redirect_kwargs.setdefault('text', self.DEFAULT_REDIRECT_BODY)
for s, d in zip(self.REDIRECT_CHAIN, self.REDIRECT_CHAIN[1:]):
- self.requests.register_uri(method, s, status_code=status_code,
- headers={'Location': d},
- **redirect_kwargs)
+ 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.register_uri(method, self.REDIRECT_CHAIN[-1],
- **final_kwargs)
+ self.requests_mock.register_uri(method, self.REDIRECT_CHAIN[-1],
+ **final_kwargs)
def assertResponse(self, resp):
self.assertEqual(resp.status_code, 200)
@@ -405,7 +424,7 @@ class SessionAuthTests(utils.TestCase):
base_url = AuthPlugin.SERVICE_URLS[service_type][interface]
uri = "%s/%s" % (base_url.rstrip('/'), path.lstrip('/'))
- self.requests.register_uri(method, uri, **kwargs)
+ 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)
@@ -446,7 +465,7 @@ class SessionAuthTests(utils.TestCase):
endpoint_filter={'service_type': service_type,
'interface': interface})
- self.assertEqual(self.requests.last_request.url,
+ 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)
@@ -468,7 +487,7 @@ class SessionAuthTests(utils.TestCase):
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.get(self.TEST_URL, status_code=401)
+ self.requests_mock.get(self.TEST_URL, status_code=401)
sess = client_session.Session()
self.assertRaises(exceptions.Unauthorized, sess.get, self.TEST_URL)
@@ -480,8 +499,8 @@ class SessionAuthTests(utils.TestCase):
passed = CalledAuthPlugin()
sess = client_session.Session()
- self.requests.get(CalledAuthPlugin.ENDPOINT + 'path',
- status_code=200)
+ self.requests_mock.get(CalledAuthPlugin.ENDPOINT + 'path',
+ status_code=200)
endpoint_filter = {'service_type': 'identity'}
# no plugin with authenticated won't work
@@ -504,8 +523,8 @@ class SessionAuthTests(utils.TestCase):
sess = client_session.Session(fixed)
- self.requests.get(CalledAuthPlugin.ENDPOINT + 'path',
- status_code=200)
+ self.requests_mock.get(CalledAuthPlugin.ENDPOINT + 'path',
+ status_code=200)
resp = sess.get('path', auth=passed,
endpoint_filter={'service_type': 'identity'})
@@ -537,9 +556,9 @@ class SessionAuthTests(utils.TestCase):
auth = CalledAuthPlugin(invalidate=True)
sess = client_session.Session(auth=auth)
- self.requests.get(self.TEST_URL,
- [{'text': 'Failed', 'status_code': 401},
- {'text': 'Hello', 'status_code': 200}])
+ 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)
@@ -552,9 +571,9 @@ class SessionAuthTests(utils.TestCase):
auth = CalledAuthPlugin(invalidate=True)
sess = client_session.Session(auth=auth)
- self.requests.get(self.TEST_URL,
- [{'text': 'Failed', 'status_code': 401},
- {'text': 'Hello', 'status_code': 200}])
+ 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)
@@ -569,14 +588,14 @@ class SessionAuthTests(utils.TestCase):
override_url = override_base + path
resp_text = uuid.uuid4().hex
- self.requests.get(override_url, text=resp_text)
+ 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.last_request.url)
+ self.assertEqual(override_url, self.requests_mock.last_request.url)
self.assertTrue(auth.get_token_called)
self.assertFalse(auth.get_endpoint_called)
@@ -589,14 +608,14 @@ class SessionAuthTests(utils.TestCase):
url = self.TEST_URL + path
resp_text = uuid.uuid4().hex
- self.requests.get(url, text=resp_text)
+ 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.last_request.url)
+ self.assertEqual(url, self.requests_mock.last_request.url)
self.assertTrue(auth.get_token_called)
self.assertFalse(auth.get_endpoint_called)
@@ -608,6 +627,34 @@ class SessionAuthTests(utils.TestCase):
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
+
+ self.stub_url('GET',
+ text=response,
+ headers={'Content-Type': 'text/html'})
+
+ resp = sess.get(self.TEST_URL, logger=logger)
+
+ self.assertEqual(response, resp.text)
+ output = io.getvalue()
+
+ self.assertIn(self.TEST_URL, output)
+ self.assertIn(response, output)
+
+ self.assertNotIn(self.TEST_URL, self.logger.output)
+ self.assertNotIn(response, self.logger.output)
+
class AdapterTest(utils.TestCase):
@@ -718,12 +765,12 @@ class AdapterTest(utils.TestCase):
adpt = adapter.Adapter(sess, endpoint_override=endpoint_override)
response = uuid.uuid4().hex
- self.requests.get(endpoint_url, text=response)
+ self.requests_mock.get(endpoint_url, text=response)
resp = adpt.get(path)
self.assertEqual(response, resp.text)
- self.assertEqual(endpoint_url, self.requests.last_request.url)
+ self.assertEqual(endpoint_url, self.requests_mock.last_request.url)
self.assertEqual(endpoint_override, adpt.get_endpoint())
@@ -760,7 +807,7 @@ class AdapterTest(utils.TestCase):
self.assertEqual(retries, m.call_count)
# we count retries so there will be one initial request + 2 retries
- self.assertThat(self.requests.request_history,
+ self.assertThat(self.requests_mock.request_history,
matchers.HasLength(retries + 1))
def test_user_and_project_id(self):
@@ -771,6 +818,35 @@ class AdapterTest(utils.TestCase):
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
+
+ self.stub_url('GET', text=response,
+ headers={'Content-Type': 'text/html'})
+
+ resp = adpt.get(self.TEST_URL, logger=logger)
+
+ self.assertEqual(response, resp.text)
+ output = io.getvalue()
+
+ self.assertIn(self.TEST_URL, output)
+ self.assertIn(response, output)
+
+ self.assertNotIn(self.TEST_URL, self.logger.output)
+ self.assertNotIn(response, self.logger.output)
+
class ConfLoadingTests(utils.TestCase):
diff --git a/keystoneclient/tests/unit/utils.py b/keystoneclient/tests/unit/utils.py
index 038f34c..b3405fb 100644
--- a/keystoneclient/tests/unit/utils.py
+++ b/keystoneclient/tests/unit/utils.py
@@ -48,7 +48,7 @@ class TestCase(testtools.TestCase):
self.time_patcher = mock.patch.object(time, 'time', lambda: 1234)
self.time_patcher.start()
- self.requests = self.useFixture(fixture.Fixture())
+ self.requests_mock = self.useFixture(fixture.Fixture())
def tearDown(self):
self.time_patcher.stop()
@@ -71,10 +71,10 @@ class TestCase(testtools.TestCase):
url = base_url
url = url.replace("/?", "?")
- self.requests.register_uri(method, url, **kwargs)
+ self.requests_mock.register_uri(method, url, **kwargs)
def assertRequestBodyIs(self, body=None, json=None):
- last_request_body = self.requests.last_request.body
+ last_request_body = self.requests_mock.last_request.body
if json:
val = jsonutils.loads(last_request_body)
self.assertEqual(json, val)
@@ -87,7 +87,7 @@ class TestCase(testtools.TestCase):
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.last_request.url)
+ parts = urlparse.urlparse(self.requests_mock.last_request.url)
querystring = urlparse.parse_qs(parts.query, keep_blank_values=True)
self.assertEqual(expected, querystring)
@@ -101,7 +101,7 @@ class TestCase(testtools.TestCase):
verified is that the parameter is present.
"""
- parts = urlparse.urlparse(self.requests.last_request.url)
+ parts = urlparse.urlparse(self.requests_mock.last_request.url)
qs = urlparse.parse_qs(parts.query, keep_blank_values=True)
for k, v in six.iteritems(kwargs):
@@ -113,7 +113,7 @@ class TestCase(testtools.TestCase):
The request must have already been made.
"""
- headers = self.requests.last_request.headers
+ headers = self.requests_mock.last_request.headers
self.assertEqual(headers.get(name), val)
diff --git a/keystoneclient/tests/unit/v2_0/client_fixtures.py b/keystoneclient/tests/unit/v2_0/client_fixtures.py
index 39d808e..0ce318d 100644
--- a/keystoneclient/tests/unit/v2_0/client_fixtures.py
+++ b/keystoneclient/tests/unit/v2_0/client_fixtures.py
@@ -11,6 +11,7 @@
# under the License.
from __future__ import unicode_literals
+import uuid
from keystoneclient import fixture
@@ -30,7 +31,8 @@ def project_scoped_token():
tenant_id='225da22d3ce34b15877ea70b2a575f58',
tenant_name='exampleproject',
user_id='c4da488862bd435c9e6c0275a0d0e49a',
- user_name='exampleuser')
+ user_name='exampleuser',
+ audit_chain_id=uuid.uuid4().hex)
f.add_role(id='member_id', name='Member')
@@ -73,7 +75,8 @@ def auth_response_body():
tenant_id='345',
tenant_name='My Project',
user_id='123',
- user_name='jqsmith')
+ user_name='jqsmith',
+ audit_chain_id=uuid.uuid4().hex)
f.add_role(id='234', name='compute:admin')
role = f.add_role(id='235', name='object-store:admin')
diff --git a/keystoneclient/tests/unit/v2_0/test_access.py b/keystoneclient/tests/unit/v2_0/test_access.py
index f05f138..e966874 100644
--- a/keystoneclient/tests/unit/v2_0/test_access.py
+++ b/keystoneclient/tests/unit/v2_0/test_access.py
@@ -61,6 +61,10 @@ class AccessInfoTest(utils.TestCase, testresources.ResourcedTestCase):
self.assertEqual(auth_ref.expires, token.expires)
self.assertEqual(auth_ref.issued, token.issued)
+ self.assertEqual(token.audit_id, auth_ref.audit_id)
+ self.assertIsNone(auth_ref.audit_chain_id)
+ self.assertIsNone(token.audit_chain_id)
+
def test_will_expire_soon(self):
token = client_fixtures.unscoped_token()
expires = timeutils.utcnow() + datetime.timedelta(minutes=5)
@@ -106,6 +110,9 @@ class AccessInfoTest(utils.TestCase, testresources.ResourcedTestCase):
self.assertTrue(auth_ref.project_scoped)
self.assertFalse(auth_ref.domain_scoped)
+ self.assertEqual(token.audit_id, auth_ref.audit_id)
+ self.assertEqual(token.audit_chain_id, auth_ref.audit_chain_id)
+
def test_diablo_token(self):
diablo_token = self.examples.TOKEN_RESPONSES[
self.examples.VALID_DIABLO_TOKEN]
diff --git a/keystoneclient/tests/unit/v2_0/test_auth.py b/keystoneclient/tests/unit/v2_0/test_auth.py
index e61f5c8..2c69dc3 100644
--- a/keystoneclient/tests/unit/v2_0/test_auth.py
+++ b/keystoneclient/tests/unit/v2_0/test_auth.py
@@ -159,13 +159,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
cl = client.Client(auth_url=self.TEST_URL,
token=fake_token)
- json_body = jsonutils.loads(self.requests.last_request.body)
+ json_body = jsonutils.loads(self.requests_mock.last_request.body)
self.assertEqual(json_body['auth']['token']['id'], fake_token)
resp, body = cl.get(fake_url)
self.assertEqual(fake_resp, body)
- token = self.requests.last_request.headers.get('X-Auth-Token')
+ token = self.requests_mock.last_request.headers.get('X-Auth-Token')
self.assertEqual(self.TEST_TOKEN, token)
def test_authenticate_success_token_scoped(self):
@@ -236,7 +236,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
resp, body = cl.get(fake_url)
self.assertEqual(fake_resp, body)
- token = self.requests.last_request.headers.get('X-Auth-Token')
+ token = self.requests_mock.last_request.headers.get('X-Auth-Token')
self.assertEqual(self.TEST_TOKEN, token)
# then override that token and the new token shall be used
@@ -245,7 +245,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
resp, body = cl.get(fake_url)
self.assertEqual(fake_resp, body)
- token = self.requests.last_request.headers.get('X-Auth-Token')
+ token = self.requests_mock.last_request.headers.get('X-Auth-Token')
self.assertEqual(fake_token, token)
# if we clear that overridden token then we fall back to the original
@@ -254,5 +254,5 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
resp, body = cl.get(fake_url)
self.assertEqual(fake_resp, body)
- token = self.requests.last_request.headers.get('X-Auth-Token')
+ token = self.requests_mock.last_request.headers.get('X-Auth-Token')
self.assertEqual(self.TEST_TOKEN, token)
diff --git a/keystoneclient/tests/unit/v2_0/test_service_catalog.py b/keystoneclient/tests/unit/v2_0/test_service_catalog.py
index e9ebf50..fddda6d 100644
--- a/keystoneclient/tests/unit/v2_0/test_service_catalog.py
+++ b/keystoneclient/tests/unit/v2_0/test_service_catalog.py
@@ -12,6 +12,7 @@
from keystoneclient import access
from keystoneclient import exceptions
+from keystoneclient import fixture
from keystoneclient.tests.unit.v2_0 import client_fixtures
from keystoneclient.tests.unit.v2_0 import utils
@@ -173,3 +174,27 @@ class ServiceCatalogTest(utils.TestCase):
endpoint_type='public')
self.assertIsNone(urls)
+
+ def test_service_catalog_multiple_service_types(self):
+ token = fixture.V2Token()
+ token.set_scope()
+
+ for i in range(3):
+ s = token.add_service('compute')
+ s.add_endpoint(public='public-%d' % i,
+ admin='admin-%d' % i,
+ internal='internal-%d' % i,
+ region='region-%d' % i)
+
+ auth_ref = access.AccessInfo.factory(resp=None, body=token)
+
+ urls = auth_ref.service_catalog.get_urls(service_type='compute',
+ endpoint_type='publicURL')
+
+ self.assertEqual(set(['public-0', 'public-1', 'public-2']), set(urls))
+
+ urls = auth_ref.service_catalog.get_urls(service_type='compute',
+ endpoint_type='publicURL',
+ region_name='region-1')
+
+ self.assertEqual(('public-1', ), urls)
diff --git a/keystoneclient/tests/unit/v2_0/test_shell.py b/keystoneclient/tests/unit/v2_0/test_shell.py
index be91d23..85bbca5 100644
--- a/keystoneclient/tests/unit/v2_0/test_shell.py
+++ b/keystoneclient/tests/unit/v2_0/test_shell.py
@@ -80,9 +80,9 @@ class ShellTests(utils.TestCase):
return out
def assert_called(self, method, path, base_url=TEST_URL):
- self.assertEqual(method, self.requests.last_request.method)
+ self.assertEqual(method, self.requests_mock.last_request.method)
self.assertEqual(base_url + path.lstrip('/'),
- self.requests.last_request.url)
+ self.requests_mock.last_request.url)
def test_user_list(self):
self.stub_url('GET', ['users'], json={'users': []})
@@ -394,7 +394,7 @@ class ShellTests(utils.TestCase):
def called_anytime(method, path, json=None):
test_url = self.TEST_URL.strip('/')
- for r in self.requests.request_history:
+ for r in self.requests_mock.request_history:
if not r.method == method:
continue
if not r.url == test_url + path:
diff --git a/keystoneclient/tests/unit/v3/client_fixtures.py b/keystoneclient/tests/unit/v3/client_fixtures.py
index 517f9ae..99e49f0 100644
--- a/keystoneclient/tests/unit/v3/client_fixtures.py
+++ b/keystoneclient/tests/unit/v3/client_fixtures.py
@@ -11,6 +11,7 @@
# under the License.
from __future__ import unicode_literals
+import uuid
from keystoneclient import fixture
@@ -30,7 +31,8 @@ def domain_scoped_token():
user_domain_name='exampledomain',
expires='2010-11-01T03:32:15-05:00',
domain_id='8e9283b7ba0b1038840c3842058b86ab',
- domain_name='anotherdomain')
+ domain_name='anotherdomain',
+ audit_chain_id=uuid.uuid4().hex)
f.add_role(id='76e72a', name='admin')
f.add_role(id='f4f392', name='member')
@@ -78,7 +80,8 @@ def project_scoped_token():
project_id='225da22d3ce34b15877ea70b2a575f58',
project_name='exampleproject',
project_domain_id='4e6893b7ba0b4006840c3845660b86ed',
- project_domain_name='exampledomain')
+ project_domain_name='exampledomain',
+ audit_chain_id=uuid.uuid4().hex)
f.add_role(id='76e72a', name='admin')
f.add_role(id='f4f392', name='member')
@@ -135,7 +138,8 @@ def auth_response_body():
project_domain_id='123',
project_domain_name='aDomain',
project_id='345',
- project_name='aTenant')
+ project_name='aTenant',
+ audit_chain_id=uuid.uuid4().hex)
f.add_role(id='76e72a', name='admin')
f.add_role(id='f4f392', name='member')
@@ -179,4 +183,5 @@ def trust_token():
trust_id='fe0aef',
trust_impersonation=False,
trustee_user_id='0ca8f6',
- trustor_user_id='bd263c')
+ trustor_user_id='bd263c',
+ audit_chain_id=uuid.uuid4().hex)
diff --git a/keystoneclient/tests/unit/v3/saml2_fixtures.py b/keystoneclient/tests/unit/v3/saml2_fixtures.py
index 2ecae6a..f428a87 100644
--- a/keystoneclient/tests/unit/v3/saml2_fixtures.py
+++ b/keystoneclient/tests/unit/v3/saml2_fixtures.py
@@ -169,3 +169,113 @@ DOMAINS = {
"next": 'null'
}
}
+
+SAML_ENCODING = "<?xml version='1.0' encoding='UTF-8'?>"
+
+TOKEN_SAML_RESPONSE = """
+<ns2:Response Destination="http://beta.example.com/Shibboleth.sso/POST/ECP"
+ ID="8c21de08d2f2435c9acf13e72c982846"
+ IssueInstant="2015-03-25T14:43:21Z"
+ Version="2.0">
+ <saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">
+ http://keystone.idp/v3/OS-FEDERATION/saml2/idp
+ </saml:Issuer>
+ <ns2:Status>
+ <ns2:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+ </ns2:Status>
+ <saml:Assertion ID="a5f02efb0bff4044b294b4583c7dfc5d"
+ IssueInstant="2015-03-25T14:43:21Z" Version="2.0">
+ <saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">
+ http://keystone.idp/v3/OS-FEDERATION/saml2/idp</saml:Issuer>
+ <xmldsig:Signature>
+ <xmldsig:SignedInfo>
+ <xmldsig:CanonicalizationMethod
+ Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+ <xmldsig:SignatureMethod
+ Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
+ <xmldsig:Reference URI="#a5f02efb0bff4044b294b4583c7dfc5d">
+ <xmldsig:Transforms>
+ <xmldsig:Transform
+ Algorithm="http://www.w3.org/2000/09/xmldsig#
+ enveloped-signature"/>
+ <xmldsig:Transform
+ Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+ </xmldsig:Transforms>
+ <xmldsig:DigestMethod
+ Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
+ <xmldsig:DigestValue>
+ 0KH2CxdkfzU+6eiRhTC+mbObUKI=
+ </xmldsig:DigestValue>
+ </xmldsig:Reference>
+ </xmldsig:SignedInfo>
+ <xmldsig:SignatureValue>
+ m2jh5gDvX/1k+4uKtbb08CHp2b9UWsLw
+ </xmldsig:SignatureValue>
+ <xmldsig:KeyInfo>
+ <xmldsig:X509Data>
+ <xmldsig:X509Certificate>...</xmldsig:X509Certificate>
+ </xmldsig:X509Data>
+ </xmldsig:KeyInfo>
+ </xmldsig:Signature>
+ <saml:Subject>
+ <saml:NameID>admin</saml:NameID>
+ <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+ <saml:SubjectConfirmationData
+ NotOnOrAfter="2015-03-25T15:43:21.172385Z"
+ Recipient="http://beta.example.com/Shibboleth.sso/POST/ECP"/>
+ </saml:SubjectConfirmation>
+ </saml:Subject>
+ <saml:AuthnStatement AuthnInstant="2015-03-25T14:43:21Z"
+ SessionIndex="9790eb729858456f8a33b7a11f0a637e"
+ SessionNotOnOrAfter="2015-03-25T15:43:21.172385Z">
+ <saml:AuthnContext>
+ <saml:AuthnContextClassRef>
+ urn:oasis:names:tc:SAML:2.0:ac:classes:Password
+ </saml:AuthnContextClassRef>
+ <saml:AuthenticatingAuthority>
+ http://keystone.idp/v3/OS-FEDERATION/saml2/idp
+ </saml:AuthenticatingAuthority>
+ </saml:AuthnContext>
+ </saml:AuthnStatement>
+ <saml:AttributeStatement>
+ <saml:Attribute Name="openstack_user"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+ <saml:AttributeValue xsi:type="xs:string">admin</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name="openstack_roles"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+ <saml:AttributeValue xsi:type="xs:string">admin</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name="openstack_project"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+ <saml:AttributeValue xsi:type="xs:string">admin</saml:AttributeValue>
+ </saml:Attribute>
+ </saml:AttributeStatement>
+ </saml:Assertion>
+</ns2:Response>
+"""
+
+TOKEN_BASED_SAML = ''.join([SAML_ENCODING, TOKEN_SAML_RESPONSE])
+
+ECP_ENVELOPE = """
+<ns0:Envelope
+ xmlns:ns0="http://schemas.xmlsoap.org/soap/envelope/"
+ xmlns:ns1="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"
+ xmlns:ns2="urn:oasis:names:tc:SAML:2.0:protocol"
+ xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+ xmlns:xmldsig="http://www.w3.org/2000/09/xmldsig#"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <ns0:Header>
+ <ns1:RelayState
+ ns0:actor="http://schemas.xmlsoap.org/soap/actor/next"
+ ns0:mustUnderstand="1">
+ ss:mem:1ddfe8b0f58341a5a840d2e8717b0737
+ </ns1:RelayState>
+ </ns0:Header>
+ <ns0:Body>
+ {0}
+ </ns0:Body>
+</ns0:Envelope>
+""".format(TOKEN_SAML_RESPONSE)
+
+TOKEN_BASED_ECP = ''.join([SAML_ENCODING, ECP_ENVELOPE])
diff --git a/keystoneclient/tests/unit/v3/test_access.py b/keystoneclient/tests/unit/v3/test_access.py
index d3107af..f069f71 100644
--- a/keystoneclient/tests/unit/v3/test_access.py
+++ b/keystoneclient/tests/unit/v3/test_access.py
@@ -70,13 +70,17 @@ class AccessInfoTest(utils.TestCase):
self.assertEqual(auth_ref.expires, UNSCOPED_TOKEN.expires)
self.assertEqual(auth_ref.issued, UNSCOPED_TOKEN.issued)
+ self.assertEqual(auth_ref.audit_id, UNSCOPED_TOKEN.audit_id)
+ self.assertIsNone(auth_ref.audit_chain_id)
+ self.assertIsNone(UNSCOPED_TOKEN.audit_chain_id)
+
def test_will_expire_soon(self):
expires = timeutils.utcnow() + datetime.timedelta(minutes=5)
UNSCOPED_TOKEN['token']['expires_at'] = expires.isoformat()
auth_ref = access.AccessInfo.factory(resp=TOKEN_RESPONSE,
body=UNSCOPED_TOKEN)
self.assertFalse(auth_ref.will_expire_soon(stale_duration=120))
- self.assertTrue(auth_ref.will_expire_soon(stale_duration=300))
+ self.assertTrue(auth_ref.will_expire_soon(stale_duration=301))
self.assertFalse(auth_ref.will_expire_soon())
def test_building_domain_scoped_accessinfo(self):
@@ -113,6 +117,10 @@ class AccessInfoTest(utils.TestCase):
self.assertTrue(auth_ref.domain_scoped)
self.assertFalse(auth_ref.project_scoped)
+ self.assertEqual(DOMAIN_SCOPED_TOKEN.audit_id, auth_ref.audit_id)
+ self.assertEqual(DOMAIN_SCOPED_TOKEN.audit_chain_id,
+ auth_ref.audit_chain_id)
+
def test_building_project_scoped_accessinfo(self):
auth_ref = access.AccessInfo.factory(resp=TOKEN_RESPONSE,
body=PROJECT_SCOPED_TOKEN)
@@ -156,6 +164,10 @@ class AccessInfoTest(utils.TestCase):
self.assertFalse(auth_ref.domain_scoped)
self.assertTrue(auth_ref.project_scoped)
+ self.assertEqual(PROJECT_SCOPED_TOKEN.audit_id, auth_ref.audit_id)
+ self.assertEqual(PROJECT_SCOPED_TOKEN.audit_chain_id,
+ auth_ref.audit_chain_id)
+
def test_oauth_access(self):
consumer_id = uuid.uuid4().hex
access_token_id = uuid.uuid4().hex
diff --git a/keystoneclient/tests/unit/v3/test_auth.py b/keystoneclient/tests/unit/v3/test_auth.py
index 506b026..b3f29d6 100644
--- a/keystoneclient/tests/unit/v3/test_auth.py
+++ b/keystoneclient/tests/unit/v3/test_auth.py
@@ -226,13 +226,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
cl = client.Client(auth_url=self.TEST_URL,
token=fake_token)
- body = jsonutils.loads(self.requests.last_request.body)
+ body = jsonutils.loads(self.requests_mock.last_request.body)
self.assertEqual(body['auth']['identity']['token']['id'], fake_token)
resp, body = cl.get(fake_url)
self.assertEqual(fake_resp, body)
- token = self.requests.last_request.headers.get('X-Auth-Token')
+ token = self.requests_mock.last_request.headers.get('X-Auth-Token')
self.assertEqual(self.TEST_TOKEN, token)
def test_authenticate_success_token_domain_scoped(self):
@@ -330,7 +330,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
resp, body = cl.get(fake_url)
self.assertEqual(fake_resp, body)
- token = self.requests.last_request.headers.get('X-Auth-Token')
+ token = self.requests_mock.last_request.headers.get('X-Auth-Token')
self.assertEqual(self.TEST_TOKEN, token)
# then override that token and the new token shall be used
@@ -339,7 +339,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
resp, body = cl.get(fake_url)
self.assertEqual(fake_resp, body)
- token = self.requests.last_request.headers.get('X-Auth-Token')
+ token = self.requests_mock.last_request.headers.get('X-Auth-Token')
self.assertEqual(fake_token, token)
# if we clear that overridden token then we fall back to the original
@@ -348,5 +348,5 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
resp, body = cl.get(fake_url)
self.assertEqual(fake_resp, body)
- token = self.requests.last_request.headers.get('X-Auth-Token')
+ token = self.requests_mock.last_request.headers.get('X-Auth-Token')
self.assertEqual(self.TEST_TOKEN, token)
diff --git a/keystoneclient/tests/unit/v3/test_auth_saml2.py b/keystoneclient/tests/unit/v3/test_auth_saml2.py
index c54cf24..d64d962 100644
--- a/keystoneclient/tests/unit/v3/test_auth_saml2.py
+++ b/keystoneclient/tests/unit/v3/test_auth_saml2.py
@@ -25,6 +25,7 @@ from keystoneclient import session
from keystoneclient.tests.unit.v3 import client_fixtures
from keystoneclient.tests.unit.v3 import saml2_fixtures
from keystoneclient.tests.unit.v3 import utils
+from keystoneclient.v3.contrib.federation import saml as saml_manager
ROOTDIR = os.path.dirname(os.path.abspath(__file__))
XMLDIR = os.path.join(ROOTDIR, 'examples', 'xml/')
@@ -128,7 +129,7 @@ class AuthenticateviaSAML2Tests(utils.TestCase):
def test_initial_sp_call(self):
"""Test initial call, expect SOAP message."""
- self.requests.get(
+ self.requests_mock.get(
self.FEDERATION_AUTH_URL,
content=make_oneline(saml2_fixtures.SP_SOAP_RESPONSE))
a = self.saml2plugin._send_service_provider_request(self.session)
@@ -153,7 +154,7 @@ class AuthenticateviaSAML2Tests(utils.TestCase):
str(self.saml2plugin.sp_response_consumer_url)))
def test_initial_sp_call_when_saml_authenticated(self):
- self.requests.get(
+ self.requests_mock.get(
self.FEDERATION_AUTH_URL,
json=saml2_fixtures.UNSCOPED_TOKEN,
headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER})
@@ -168,7 +169,7 @@ class AuthenticateviaSAML2Tests(utils.TestCase):
self.saml2plugin.authenticated_response.headers['X-Subject-Token'])
def test_get_unscoped_token_when_authenticated(self):
- self.requests.get(
+ self.requests_mock.get(
self.FEDERATION_AUTH_URL,
json=saml2_fixtures.UNSCOPED_TOKEN,
headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER,
@@ -181,8 +182,8 @@ class AuthenticateviaSAML2Tests(utils.TestCase):
def test_initial_sp_call_invalid_response(self):
"""Send initial SP HTTP request and receive wrong server response."""
- self.requests.get(self.FEDERATION_AUTH_URL,
- text='NON XML RESPONSE')
+ self.requests_mock.get(self.FEDERATION_AUTH_URL,
+ text='NON XML RESPONSE')
self.assertRaises(
exceptions.AuthorizationFailure,
@@ -190,8 +191,8 @@ class AuthenticateviaSAML2Tests(utils.TestCase):
self.session)
def test_send_authn_req_to_idp(self):
- self.requests.post(self.IDENTITY_PROVIDER_URL,
- content=saml2_fixtures.SAML2_ASSERTION)
+ self.requests_mock.post(self.IDENTITY_PROVIDER_URL,
+ content=saml2_fixtures.SAML2_ASSERTION)
self.saml2plugin.sp_response_consumer_url = self.SHIB_CONSUMER_URL
self.saml2plugin.saml2_authn_request = etree.XML(
@@ -208,7 +209,7 @@ class AuthenticateviaSAML2Tests(utils.TestCase):
self.assertEqual(idp_response, saml2_assertion_oneline, error)
def test_fail_basicauth_idp_authentication(self):
- self.requests.post(self.IDENTITY_PROVIDER_URL, status_code=401)
+ self.requests_mock.post(self.IDENTITY_PROVIDER_URL, status_code=401)
self.saml2plugin.sp_response_consumer_url = self.SHIB_CONSUMER_URL
self.saml2plugin.saml2_authn_request = etree.XML(
@@ -225,7 +226,7 @@ class AuthenticateviaSAML2Tests(utils.TestCase):
self.IDENTITY_PROVIDER_URL)
def test_send_authn_response_to_sp(self):
- self.requests.post(
+ self.requests_mock.post(
self.SHIB_CONSUMER_URL,
json=saml2_fixtures.UNSCOPED_TOKEN,
headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER})
@@ -255,7 +256,7 @@ class AuthenticateviaSAML2Tests(utils.TestCase):
self.SHIB_CONSUMER_URL)
def test_consumer_url_mismatch(self):
- self.requests.post(self.SHIB_CONSUMER_URL)
+ self.requests_mock.post(self.SHIB_CONSUMER_URL)
invalid_consumer_url = uuid.uuid4().hex
self.assertRaises(
exceptions.ValidationError,
@@ -264,13 +265,13 @@ class AuthenticateviaSAML2Tests(utils.TestCase):
invalid_consumer_url)
def test_custom_302_redirection(self):
- self.requests.post(
+ self.requests_mock.post(
self.SHIB_CONSUMER_URL,
text='BODY',
headers={'location': self.FEDERATION_AUTH_URL},
status_code=302)
- self.requests.get(
+ self.requests_mock.get(
self.FEDERATION_AUTH_URL,
json=saml2_fixtures.UNSCOPED_TOKEN,
headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER})
@@ -289,14 +290,14 @@ class AuthenticateviaSAML2Tests(utils.TestCase):
self.assertEqual('GET', response.request.method)
def test_end_to_end_workflow(self):
- self.requests.get(
+ self.requests_mock.get(
self.FEDERATION_AUTH_URL,
content=make_oneline(saml2_fixtures.SP_SOAP_RESPONSE))
- self.requests.post(self.IDENTITY_PROVIDER_URL,
- content=saml2_fixtures.SAML2_ASSERTION)
+ self.requests_mock.post(self.IDENTITY_PROVIDER_URL,
+ content=saml2_fixtures.SAML2_ASSERTION)
- self.requests.post(
+ self.requests_mock.post(
self.SHIB_CONSUMER_URL,
json=saml2_fixtures.UNSCOPED_TOKEN,
headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER,
@@ -463,7 +464,7 @@ class AuthenticateviaADFSTests(utils.TestCase):
def test_get_adfs_security_token(self):
"""Test ADFSUnscopedToken._get_adfs_security_token()."""
- self.requests.post(
+ self.requests_mock.post(
self.IDENTITY_PROVIDER_URL,
content=make_oneline(self.ADFS_SECURITY_TOKEN_RESPONSE),
status_code=200)
@@ -526,9 +527,9 @@ class AuthenticateviaADFSTests(utils.TestCase):
An exceptions.AuthorizationFailure should be raised including
error message from the XML message indicating where was the problem.
"""
- self.requests.post(self.IDENTITY_PROVIDER_URL,
- content=make_oneline(self.ADFS_FAULT),
- status_code=500)
+ self.requests_mock.post(self.IDENTITY_PROVIDER_URL,
+ content=make_oneline(self.ADFS_FAULT),
+ status_code=500)
self.adfsplugin._prepare_adfs_request()
self.assertRaises(exceptions.AuthorizationFailure,
@@ -545,9 +546,9 @@ class AuthenticateviaADFSTests(utils.TestCase):
and correctly raise exceptions.InternalServerError once it cannot
parse XML fault message
"""
- self.requests.post(self.IDENTITY_PROVIDER_URL,
- content=b'NOT XML',
- status_code=500)
+ self.requests_mock.post(self.IDENTITY_PROVIDER_URL,
+ content=b'NOT XML',
+ status_code=500)
self.adfsplugin._prepare_adfs_request()
self.assertRaises(exceptions.InternalServerError,
self.adfsplugin._get_adfs_security_token,
@@ -559,9 +560,9 @@ class AuthenticateviaADFSTests(utils.TestCase):
"""Test whether SP issues a cookie."""
cookie = uuid.uuid4().hex
- self.requests.post(self.SP_ENDPOINT,
- headers={"set-cookie": cookie},
- status_code=302)
+ 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()
@@ -570,7 +571,7 @@ class AuthenticateviaADFSTests(utils.TestCase):
self.assertEqual(1, len(self.session.session.cookies))
def test_send_assertion_to_service_provider_bad_status(self):
- self.requests.post(self.SP_ENDPOINT, status_code=500)
+ self.requests_mock.post(self.SP_ENDPOINT, status_code=500)
self.adfsplugin.adfs_token = etree.XML(
self.ADFS_SECURITY_TOKEN_RESPONSE)
@@ -582,19 +583,22 @@ class AuthenticateviaADFSTests(utils.TestCase):
self.session)
def test_access_sp_no_cookies_fail(self):
- # clean cookie jar
- self.session.session.cookies = []
-
+ # There are no cookies in the session initially, and
+ # _access_service_provider requires a cookie in the session.
self.assertRaises(exceptions.AuthorizationFailure,
self.adfsplugin._access_service_provider,
self.session)
def test_check_valid_token_when_authenticated(self):
- self.requests.get(self.FEDERATION_AUTH_URL,
- json=saml2_fixtures.UNSCOPED_TOKEN,
- headers=client_fixtures.AUTH_RESPONSE_HEADERS)
+ self.requests_mock.get(self.FEDERATION_AUTH_URL,
+ json=saml2_fixtures.UNSCOPED_TOKEN,
+ headers=client_fixtures.AUTH_RESPONSE_HEADERS)
+
+ # _access_service_provider requires a cookie in the session.
+ cookie = requests.cookies.create_cookie(
+ name=self.getUniqueString(), value=self.getUniqueString())
+ self.session.session.cookies.set_cookie(cookie)
- self.session.session.cookies = [object()]
self.adfsplugin._access_service_provider(self.session)
response = self.adfsplugin.authenticated_response
@@ -605,19 +609,79 @@ class AuthenticateviaADFSTests(utils.TestCase):
response.json()['token'])
def test_end_to_end_workflow(self):
- self.requests.post(self.IDENTITY_PROVIDER_URL,
- content=self.ADFS_SECURITY_TOKEN_RESPONSE,
- status_code=200)
- self.requests.post(self.SP_ENDPOINT,
- headers={"set-cookie": 'x'},
- status_code=302)
- self.requests.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 can
+ self.requests_mock.post(self.IDENTITY_PROVIDER_URL,
+ content=self.ADFS_SECURITY_TOKEN_RESPONSE,
+ status_code=200)
+ self.requests_mock.post(self.SP_ENDPOINT,
+ headers={"set-cookie": 'x'},
+ status_code=302)
+ self.requests_mock.get(self.FEDERATION_AUTH_URL,
+ json=saml2_fixtures.UNSCOPED_TOKEN,
+ headers=client_fixtures.AUTH_RESPONSE_HEADERS)
+
+ # NOTE(marek-denis): We need to mimic this until self.requests_mock can
# issue cookies properly.
- self.session.session.cookies = [object()]
+ cookie = requests.cookies.create_cookie(
+ name=self.getUniqueString(), value=self.getUniqueString())
+ self.session.session.cookies.set_cookie(cookie)
+
token, token_json = self.adfsplugin._get_unscoped_token(self.session)
self.assertEqual(token, client_fixtures.AUTH_SUBJECT_TOKEN)
self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], token_json)
+
+
+class SAMLGenerationTests(utils.TestCase):
+
+ def setUp(self):
+ super(SAMLGenerationTests, self).setUp()
+ self.manager = self.client.federation.saml
+ self.SAML2_FULL_URL = ''.join([self.TEST_URL,
+ saml_manager.SAML2_ENDPOINT])
+ self.ECP_FULL_URL = ''.join([self.TEST_URL,
+ saml_manager.ECP_ENDPOINT])
+
+ def test_saml_create(self):
+ """Test that a token can be exchanged for a SAML assertion."""
+
+ token_id = uuid.uuid4().hex
+ service_provider_id = uuid.uuid4().hex
+
+ # Mock the returned text for '/auth/OS-FEDERATION/saml2
+ self.requests_mock.post(self.SAML2_FULL_URL,
+ text=saml2_fixtures.TOKEN_BASED_SAML)
+
+ text = self.manager.create_saml_assertion(service_provider_id,
+ token_id)
+
+ # Ensure returned text is correct
+ self.assertEqual(saml2_fixtures.TOKEN_BASED_SAML, text)
+
+ # Ensure request headers and body are correct
+ req_json = self.requests_mock.last_request.json()
+ self.assertEqual(token_id, req_json['auth']['identity']['token']['id'])
+ self.assertEqual(service_provider_id,
+ req_json['auth']['scope']['service_provider']['id'])
+ self.assertRequestHeaderEqual('Content-Type', 'application/json')
+
+ def test_ecp_create(self):
+ """Test that a token can be exchanged for an ECP wrapped assertion."""
+
+ token_id = uuid.uuid4().hex
+ service_provider_id = uuid.uuid4().hex
+
+ # Mock returned text for '/auth/OS-FEDERATION/saml2/ecp
+ self.requests_mock.post(self.ECP_FULL_URL,
+ text=saml2_fixtures.TOKEN_BASED_ECP)
+
+ text = self.manager.create_ecp_assertion(service_provider_id,
+ token_id)
+
+ # Ensure returned text is correct
+ self.assertEqual(saml2_fixtures.TOKEN_BASED_ECP, text)
+
+ # Ensure request headers and body are correct
+ req_json = self.requests_mock.last_request.json()
+ self.assertEqual(token_id, req_json['auth']['identity']['token']['id'])
+ self.assertEqual(service_provider_id,
+ req_json['auth']['scope']['service_provider']['id'])
+ self.assertRequestHeaderEqual('Content-Type', 'application/json')
diff --git a/keystoneclient/tests/unit/v3/test_discover.py b/keystoneclient/tests/unit/v3/test_discover.py
index f73c3d7..ae88bb4 100644
--- a/keystoneclient/tests/unit/v3/test_discover.py
+++ b/keystoneclient/tests/unit/v3/test_discover.py
@@ -61,9 +61,9 @@ class DiscoverKeystoneTests(utils.UnauthenticatedTestCase):
}
def test_get_version_local(self):
- self.requests.get("http://localhost:35357/",
- status_code=300,
- json=self.TEST_RESPONSE_DICT)
+ self.requests_mock.get("http://localhost:35357/",
+ status_code=300,
+ json=self.TEST_RESPONSE_DICT)
cs = client.Client()
versions = cs.discover()
diff --git a/keystoneclient/tests/unit/v3/test_federation.py b/keystoneclient/tests/unit/v3/test_federation.py
index 19ec44f..ff219cc 100644
--- a/keystoneclient/tests/unit/v3/test_federation.py
+++ b/keystoneclient/tests/unit/v3/test_federation.py
@@ -21,6 +21,7 @@ from keystoneclient.v3.contrib.federation import base
from keystoneclient.v3.contrib.federation import identity_providers
from keystoneclient.v3.contrib.federation import mappings
from keystoneclient.v3.contrib.federation import protocols
+from keystoneclient.v3.contrib.federation import service_providers
from keystoneclient.v3 import domains
from keystoneclient.v3 import projects
@@ -70,14 +71,9 @@ class IdentityProviderTests(utils.TestCase, utils.CrudTests):
self.assertRaises(TypeError, getattr(self.manager, f_name),
*args)
- def test_create(self, ref=None, req_ref=None):
- ref = ref or self.new_ref()
-
- # req_ref argument allows you to specify a different
- # signature for the request when the manager does some
- # conversion before doing the request (e.g. converting
- # from datetime object to timestamp string)
- req_ref = (req_ref or ref).copy()
+ def test_create(self):
+ ref = self.new_ref()
+ req_ref = ref.copy()
req_ref.pop('id')
self.stub_entity('PUT', entity=ref, id=ref['id'], status_code=201)
@@ -107,16 +103,11 @@ class MappingTests(utils.TestCase, utils.CrudTests):
uuid.uuid4().hex])
return kwargs
- def test_create(self, ref=None, req_ref=None):
- ref = ref or self.new_ref()
+ def test_create(self):
+ ref = self.new_ref()
manager_ref = ref.copy()
mapping_id = manager_ref.pop('id')
-
- # req_ref argument allows you to specify a different
- # signature for the request when the manager does some
- # conversion before doing the request (e.g. converting
- # from datetime object to timestamp string)
- req_ref = (req_ref or ref).copy()
+ req_ref = ref.copy()
self.stub_entity('PUT', entity=req_ref, id=mapping_id,
status_code=201)
@@ -349,7 +340,7 @@ class FederationProjectTests(utils.TestCase):
projects_json = {
self.collection_key: [self.new_ref(), self.new_ref()]
}
- self.requests.get(self.URL, json=projects_json)
+ self.requests_mock.get(self.URL, json=projects_json)
returned_list = self.manager.list()
self.assertEqual(len(projects_ref), len(returned_list))
@@ -380,7 +371,7 @@ class FederationDomainTests(utils.TestCase):
domains_json = {
self.collection_key: domains_ref
}
- self.requests.get(self.URL, json=domains_json)
+ self.requests_mock.get(self.URL, json=domains_json)
returned_list = self.manager.list()
self.assertEqual(len(domains_ref), len(returned_list))
for domain in returned_list:
@@ -407,3 +398,72 @@ class FederatedTokenTests(utils.TestCase):
def test_get_user_domain_id(self):
"""Ensure a federated user's domain ID does not exist."""
self.assertIsNone(self.federated_token.user_domain_id)
+
+
+class ServiceProviderTests(utils.TestCase, utils.CrudTests):
+ def setUp(self):
+ super(ServiceProviderTests, self).setUp()
+ self.key = 'service_provider'
+ self.collection_key = 'service_providers'
+ self.model = service_providers.ServiceProvider
+ self.manager = self.client.federation.service_providers
+ self.path_prefix = 'OS-FEDERATION'
+
+ def new_ref(self, **kwargs):
+ kwargs.setdefault('auth_url', uuid.uuid4().hex)
+ kwargs.setdefault('description', uuid.uuid4().hex)
+ kwargs.setdefault('enabled', True)
+ kwargs.setdefault('id', uuid.uuid4().hex)
+ kwargs.setdefault('sp_url', uuid.uuid4().hex)
+ return kwargs
+
+ def test_positional_parameters_expect_fail(self):
+ """Ensure CrudManager raises TypeError exceptions.
+
+ After passing wrong number of positional arguments
+ an exception should be raised.
+
+ Operations to be tested:
+ * create()
+ * get()
+ * list()
+ * delete()
+ * update()
+
+ """
+ POS_PARAM_1 = uuid.uuid4().hex
+ POS_PARAM_2 = uuid.uuid4().hex
+ POS_PARAM_3 = uuid.uuid4().hex
+
+ PARAMETERS = {
+ 'create': (POS_PARAM_1, POS_PARAM_2),
+ 'get': (POS_PARAM_1, POS_PARAM_2),
+ 'list': (POS_PARAM_1, POS_PARAM_2),
+ 'update': (POS_PARAM_1, POS_PARAM_2, POS_PARAM_3),
+ 'delete': (POS_PARAM_1, POS_PARAM_2)
+ }
+
+ for f_name, args in PARAMETERS.items():
+ self.assertRaises(TypeError, getattr(self.manager, f_name),
+ *args)
+
+ def test_create(self):
+ ref = self.new_ref()
+
+ # req_ref argument allows you to specify a different
+ # signature for the request when the manager does some
+ # conversion before doing the request (e.g. converting
+ # from datetime object to timestamp string)
+ req_ref = ref.copy()
+ req_ref.pop('id')
+
+ self.stub_entity('PUT', entity=ref, id=ref['id'], status_code=201)
+
+ returned = self.manager.create(**ref)
+ self.assertIsInstance(returned, self.model)
+ for attr in req_ref:
+ self.assertEqual(
+ getattr(returned, attr),
+ req_ref[attr],
+ 'Expected different %s' % attr)
+ self.assertEntityRequestBodyIs(req_ref)
diff --git a/keystoneclient/tests/unit/v3/test_oauth1.py b/keystoneclient/tests/unit/v3/test_oauth1.py
index d259053..b52a759 100644
--- a/keystoneclient/tests/unit/v3/test_oauth1.py
+++ b/keystoneclient/tests/unit/v3/test_oauth1.py
@@ -183,7 +183,7 @@ class RequestTokenTests(TokenTests):
# Assert that the project id is in the header
self.assertRequestHeaderEqual('requested-project-id', project_id)
- req_headers = self.requests.last_request.headers
+ req_headers = self.requests_mock.last_request.headers
oauth_client = oauth1.Client(consumer_key,
client_secret=consumer_secret,
@@ -223,7 +223,7 @@ class AccessTokenTests(TokenTests):
self.assertEqual(access_secret, access_token.secret)
self.assertEqual(expires_at, access_token.expires)
- req_headers = self.requests.last_request.headers
+ req_headers = self.requests_mock.last_request.headers
oauth_client = oauth1.Client(consumer_key,
client_secret=consumer_secret,
resource_owner_key=request_key,
@@ -275,7 +275,7 @@ class AuthenticateWithOAuthTests(TokenTests):
self.assertRequestBodyIs(json=OAUTH_REQUEST_BODY)
# Assert that the headers have the same oauthlib data
- req_headers = self.requests.last_request.headers
+ req_headers = self.requests_mock.last_request.headers
oauth_client = oauth1.Client(consumer_key,
client_secret=consumer_secret,
resource_owner_key=access_key,
diff --git a/keystoneclient/tests/unit/v3/test_projects.py b/keystoneclient/tests/unit/v3/test_projects.py
index 5d08bb2..61a5ef1 100644
--- a/keystoneclient/tests/unit/v3/test_projects.py
+++ b/keystoneclient/tests/unit/v3/test_projects.py
@@ -144,6 +144,75 @@ class ProjectTests(utils.TestCase, utils.CrudTests):
return projects
+ def test_get_with_subtree_as_ids(self):
+ projects = self._create_projects_hierarchy()
+ ref = projects[0]
+
+ # We will query for projects[0] subtree, it should include projects[1]
+ # and projects[2] structured like the following:
+ # {
+ # projects[1]: {
+ # projects[2]: None
+ # }
+ # }
+ ref['subtree'] = {
+ projects[1]['id']: {
+ projects[2]['id']: None
+ }
+ }
+
+ self.stub_entity('GET', id=ref['id'], entity=ref)
+
+ returned = self.manager.get(ref['id'], subtree_as_ids=True)
+ self.assertQueryStringIs('subtree_as_ids')
+ self.assertDictEqual(ref['subtree'], returned.subtree)
+
+ def test_get_with_parents_as_ids(self):
+ projects = self._create_projects_hierarchy()
+ ref = projects[2]
+
+ # We will query for projects[2] parents, it should include projects[1]
+ # and projects[0] structured like the following:
+ # {
+ # projects[1]: {
+ # projects[0]: None
+ # }
+ # }
+ ref['parents'] = {
+ projects[1]['id']: {
+ projects[0]['id']: None
+ }
+ }
+
+ self.stub_entity('GET', id=ref['id'], entity=ref)
+
+ returned = self.manager.get(ref['id'], parents_as_ids=True)
+ self.assertQueryStringIs('parents_as_ids')
+ self.assertDictEqual(ref['parents'], returned.parents)
+
+ def test_get_with_parents_as_ids_and_subtree_as_ids(self):
+ ref = self.new_ref()
+ projects = self._create_projects_hierarchy()
+ ref = projects[1]
+
+ # We will query for projects[1] subtree and parents. The subtree should
+ # include projects[2] and the parents should include projects[2].
+ ref['parents'] = {
+ projects[0]['id']: None
+ }
+ ref['subtree'] = {
+ projects[2]['id']: None
+ }
+
+ self.stub_entity('GET', id=ref['id'], entity=ref)
+
+ returned = self.manager.get(ref['id'],
+ parents_as_ids=True,
+ subtree_as_ids=True)
+ self.assertQueryStringIs('subtree_as_ids&parents_as_ids')
+ self.assertDictEqual(ref['parents'], returned.parents)
+ self.assertDictEqual(ref['subtree'], returned.subtree)
+
def test_get_with_subtree_as_list(self):
projects = self._create_projects_hierarchy()
ref = projects[0]
@@ -213,6 +282,23 @@ class ProjectTests(utils.TestCase, utils.CrudTests):
projects[2][attr],
'Expected different %s' % attr)
+ def test_get_with_invalid_parameters_combination(self):
+ # subtree_as_list and subtree_as_ids can not be included at the
+ # same time in the call.
+ self.assertRaises(exceptions.ValidationError,
+ self.manager.get,
+ project=uuid.uuid4().hex,
+ subtree_as_list=True,
+ subtree_as_ids=True)
+
+ # parents_as_list and parents_as_ids can not be included at the
+ # same time in the call.
+ self.assertRaises(exceptions.ValidationError,
+ self.manager.get,
+ project=uuid.uuid4().hex,
+ parents_as_list=True,
+ parents_as_ids=True)
+
def test_update_with_parent_project(self):
ref = self.new_ref()
ref['parent_id'] = uuid.uuid4().hex
diff --git a/keystoneclient/tests/unit/v3/test_role_assignments.py b/keystoneclient/tests/unit/v3/test_role_assignments.py
index 1a664c9..79d2585 100644
--- a/keystoneclient/tests/unit/v3/test_role_assignments.py
+++ b/keystoneclient/tests/unit/v3/test_role_assignments.py
@@ -177,6 +177,21 @@ class RoleAssignmentsTests(utils.TestCase, utils.CrudTests):
kwargs = {'role.id': self.TEST_ROLE_ID}
self.assertQueryStringContains(**kwargs)
+ def test_role_assignments_inherited_list(self):
+ ref_list = self.TEST_ALL_RESPONSE_LIST
+ self.stub_entity('GET',
+ [self.collection_key,
+ '?scope.OS-INHERIT:inherited_to=projects'],
+ entity=ref_list
+ )
+
+ returned_list = self.manager.list(
+ os_inherit_extension_inherited_to='projects')
+ self._assert_returned_list(ref_list, returned_list)
+
+ query_string = 'scope.OS-INHERIT:inherited_to=projects'
+ self.assertQueryStringIs(query_string)
+
def test_domain_and_project_list(self):
# Should only accept either domain or project, never both
self.assertRaises(exceptions.ValidationError,
diff --git a/keystoneclient/tests/unit/v3/test_service_catalog.py b/keystoneclient/tests/unit/v3/test_service_catalog.py
index a187302..054ad56 100644
--- a/keystoneclient/tests/unit/v3/test_service_catalog.py
+++ b/keystoneclient/tests/unit/v3/test_service_catalog.py
@@ -12,6 +12,7 @@
from keystoneclient import access
from keystoneclient import exceptions
+from keystoneclient import fixture
from keystoneclient.tests.unit.v3 import client_fixtures
from keystoneclient.tests.unit.v3 import utils
@@ -216,3 +217,55 @@ class ServiceCatalogTest(utils.TestCase):
self.assertRaises(exceptions.EndpointNotFound, ab_sc.url_for,
service_type='compute', service_name='NotExist',
endpoint_type='public')
+
+
+class ServiceCatalogV3Test(ServiceCatalogTest):
+
+ def test_building_a_service_catalog(self):
+ auth_ref = access.AccessInfo.factory(self.RESPONSE,
+ self.AUTH_RESPONSE_BODY)
+ sc = auth_ref.service_catalog
+
+ self.assertEqual(sc.url_for(service_type='compute'),
+ 'https://compute.north.host/novapi/public')
+ self.assertEqual(sc.url_for(service_type='compute',
+ endpoint_type='internal'),
+ 'https://compute.north.host/novapi/internal')
+
+ self.assertRaises(exceptions.EndpointNotFound, sc.url_for, 'region_id',
+ 'South', service_type='compute')
+
+ def test_service_catalog_endpoints(self):
+ auth_ref = access.AccessInfo.factory(self.RESPONSE,
+ self.AUTH_RESPONSE_BODY)
+ sc = auth_ref.service_catalog
+
+ public_ep = sc.get_endpoints(service_type='compute',
+ endpoint_type='public')
+ self.assertEqual(public_ep['compute'][0]['region_id'], 'North')
+ self.assertEqual(public_ep['compute'][0]['url'],
+ 'https://compute.north.host/novapi/public')
+
+ def test_service_catalog_multiple_service_types(self):
+ token = fixture.V3Token()
+ token.set_project_scope()
+
+ for i in range(3):
+ s = token.add_service('compute')
+ s.add_standard_endpoints(public='public-%d' % i,
+ admin='admin-%d' % i,
+ internal='internal-%d' % i,
+ region='region-%d' % i)
+
+ auth_ref = access.AccessInfo.factory(resp=None, body=token)
+
+ urls = auth_ref.service_catalog.get_urls(service_type='compute',
+ endpoint_type='public')
+
+ self.assertEqual(set(['public-0', 'public-1', 'public-2']), set(urls))
+
+ urls = auth_ref.service_catalog.get_urls(service_type='compute',
+ endpoint_type='public',
+ region_name='region-1')
+
+ self.assertEqual(('public-1', ), urls)
diff --git a/keystoneclient/tests/unit/v3/test_simple_cert.py b/keystoneclient/tests/unit/v3/test_simple_cert.py
new file mode 100644
index 0000000..067083a
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_simple_cert.py
@@ -0,0 +1,40 @@
+# Copyright 2014 IBM Corp.
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import testresources
+
+from keystoneclient.tests.unit import client_fixtures
+from keystoneclient.tests.unit.v3 import utils
+
+
+class SimpleCertTests(utils.TestCase, testresources.ResourcedTestCase):
+
+ resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+ def test_get_ca_certificate(self):
+ self.stub_url('GET', ['OS-SIMPLE-CERT', 'ca'],
+ headers={'Content-Type': 'application/x-pem-file'},
+ text=self.examples.SIGNING_CA)
+ res = self.client.simple_cert.get_ca_certificates()
+ self.assertEqual(self.examples.SIGNING_CA, res)
+
+ def test_get_certificates(self):
+ self.stub_url('GET', ['OS-SIMPLE-CERT', 'certificates'],
+ headers={'Content-Type': 'application/x-pem-file'},
+ text=self.examples.SIGNING_CERT)
+ res = self.client.simple_cert.get_certificates()
+ self.assertEqual(self.examples.SIGNING_CERT, res)
+
+
+def load_tests(loader, tests, pattern):
+ return testresources.OptimisingTestSuite(tests)
diff --git a/keystoneclient/tests/unit/v3/test_users.py b/keystoneclient/tests/unit/v3/test_users.py
index 4645d6e..4619b66 100644
--- a/keystoneclient/tests/unit/v3/test_users.py
+++ b/keystoneclient/tests/unit/v3/test_users.py
@@ -241,7 +241,7 @@ class UserTests(utils.TestCase, utils.CrudTests):
}
self.assertEqual(self.TEST_URL + '/users/test/password',
- self.requests.last_request.url)
+ self.requests_mock.last_request.url)
self.assertRequestBodyIs(json=exp_req_body)
self.assertNotIn(old_password, self.logger.output)
self.assertNotIn(new_password, self.logger.output)
diff --git a/keystoneclient/tests/unit/v3/utils.py b/keystoneclient/tests/unit/v3/utils.py
index 7320687..7f2d633 100644
--- a/keystoneclient/tests/unit/v3/utils.py
+++ b/keystoneclient/tests/unit/v3/utils.py
@@ -250,14 +250,14 @@ class CrudTests(object):
ref_list = ref_list or [self.new_ref(), self.new_ref()]
expected_path = self._get_expected_path(expected_path)
- self.requests.get(urlparse.urljoin(self.TEST_URL, expected_path),
- json=self.encode(ref_list))
+ self.requests_mock.get(urlparse.urljoin(self.TEST_URL, expected_path),
+ json=self.encode(ref_list))
returned_list = self.manager.list(**filter_kwargs)
self.assertEqual(len(ref_list), len(returned_list))
[self.assertIsInstance(r, self.model) for r in returned_list]
- qs_args = self.requests.last_request.qs
+ qs_args = self.requests_mock.last_request.qs
qs_args_expected = expected_query or filter_kwargs
for key, value in six.iteritems(qs_args_expected):
self.assertIn(key, qs_args)
@@ -276,8 +276,8 @@ class CrudTests(object):
filter_kwargs = {uuid.uuid4().hex: uuid.uuid4().hex}
expected_path = self._get_expected_path()
- self.requests.get(urlparse.urljoin(self.TEST_URL, expected_path),
- json=self.encode(ref_list))
+ self.requests_mock.get(urlparse.urljoin(self.TEST_URL, expected_path),
+ json=self.encode(ref_list))
self.manager.list(**filter_kwargs)
self.assertQueryStringContains(**filter_kwargs)
diff --git a/keystoneclient/v3/client.py b/keystoneclient/v3/client.py
index 8becfab..f7becbb 100644
--- a/keystoneclient/v3/client.py
+++ b/keystoneclient/v3/client.py
@@ -25,6 +25,7 @@ from keystoneclient.v3.contrib import endpoint_filter
from keystoneclient.v3.contrib import endpoint_policy
from keystoneclient.v3.contrib import federation
from keystoneclient.v3.contrib import oauth1
+from keystoneclient.v3.contrib import simple_cert
from keystoneclient.v3.contrib import trusts
from keystoneclient.v3 import credentials
from keystoneclient.v3 import domains
@@ -145,6 +146,10 @@ EndpointPolicyManager`
:py:class:`keystoneclient.v3.roles.RoleManager`
+ .. py:attribute:: simple_cert
+
+ :py:class:`keystoneclient.v3.contrib.simple_cert.SimpleCertManager`
+
.. py:attribute:: services
:py:class:`keystoneclient.v3.services.ServiceManager`
@@ -186,6 +191,7 @@ EndpointPolicyManager`
role_assignments.RoleAssignmentManager(self._adapter))
self.roles = roles.RoleManager(self._adapter)
self.services = services.ServiceManager(self._adapter)
+ self.simple_cert = simple_cert.SimpleCertManager(self._adapter)
self.tokens = tokens.TokenManager(self._adapter)
self.trusts = trusts.TrustManager(self._adapter)
self.users = users.UserManager(self._adapter)
diff --git a/keystoneclient/v3/contrib/federation/core.py b/keystoneclient/v3/contrib/federation/core.py
index 76c5dc7..2e12cf6 100644
--- a/keystoneclient/v3/contrib/federation/core.py
+++ b/keystoneclient/v3/contrib/federation/core.py
@@ -15,6 +15,8 @@ from keystoneclient.v3.contrib.federation import identity_providers
from keystoneclient.v3.contrib.federation import mappings
from keystoneclient.v3.contrib.federation import projects
from keystoneclient.v3.contrib.federation import protocols
+from keystoneclient.v3.contrib.federation import saml
+from keystoneclient.v3.contrib.federation import service_providers
class FederationManager(object):
@@ -25,3 +27,5 @@ class FederationManager(object):
self.protocols = protocols.ProtocolManager(api)
self.projects = projects.ProjectManager(api)
self.domains = domains.DomainManager(api)
+ self.saml = saml.SamlManager(api)
+ self.service_providers = service_providers.ServiceProviderManager(api)
diff --git a/keystoneclient/v3/contrib/federation/saml.py b/keystoneclient/v3/contrib/federation/saml.py
new file mode 100644
index 0000000..b758354
--- /dev/null
+++ b/keystoneclient/v3/contrib/federation/saml.py
@@ -0,0 +1,81 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from keystoneclient import base
+
+
+SAML2_ENDPOINT = '/auth/OS-FEDERATION/saml2'
+ECP_ENDPOINT = '/auth/OS-FEDERATION/saml2/ecp'
+
+
+class SamlManager(base.Manager):
+ """Manager class for creating SAML assertions."""
+
+ def create_saml_assertion(self, service_provider, token_id):
+ """Create a SAML assertion from a token.
+
+ Equivalent Identity API call:
+ POST /auth/OS-FEDERATION/saml2
+
+ :param service_provider: Service Provider resource.
+ :type service_provider: string
+ :param token_id: Token to transform to SAML assertion.
+ :type token_id: string
+
+ :returns: SAML representation of token_id
+ :rtype: string
+ """
+
+ headers, body = self._create_common_request(service_provider, token_id)
+ resp, body = self.client.post(SAML2_ENDPOINT, json=body,
+ headers=headers)
+ return resp.text
+
+ def create_ecp_assertion(self, service_provider, token_id):
+ """Create an ECP wrapped SAML assertion from a token.
+
+ Equivalent Identity API call:
+ POST /auth/OS-FEDERATION/saml2/ecp
+
+ :param service_provider: Service Provider resource.
+ :type service_provider: string
+ :param token_id: Token to transform to SAML assertion.
+ :type token_id: string
+
+ :returns: SAML representation of token_id, wrapped in ECP envelope
+ :rtype: string
+ """
+
+ headers, body = self._create_common_request(service_provider, token_id)
+ resp, body = self.client.post(ECP_ENDPOINT, json=body,
+ headers=headers)
+ return resp.text
+
+ def _create_common_request(self, service_provider, token_id):
+ headers = {'Content-Type': 'application/json'}
+ body = {
+ 'auth': {
+ 'identity': {
+ 'methods': ['token'],
+ 'token': {
+ 'id': token_id
+ }
+ },
+ 'scope': {
+ 'service_provider': {
+ 'id': base.getid(service_provider)
+ }
+ }
+ }
+ }
+
+ return (headers, body)
diff --git a/keystoneclient/v3/contrib/federation/service_providers.py b/keystoneclient/v3/contrib/federation/service_providers.py
new file mode 100644
index 0000000..a419295
--- /dev/null
+++ b/keystoneclient/v3/contrib/federation/service_providers.py
@@ -0,0 +1,104 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from keystoneclient import base
+from keystoneclient import utils
+
+
+class ServiceProvider(base.Resource):
+ """Object representing Service Provider container
+
+ Attributes:
+ * id: user-defined unique string identifying Service Provider.
+ * sp_url: the shibboleth endpoint of a Service Provider.
+ * auth_url: the authentication url of Service Provider.
+
+ """
+ pass
+
+
+class ServiceProviderManager(base.CrudManager):
+ """Manager class for manipulating Service Providers."""
+
+ resource_class = ServiceProvider
+ collection_key = 'service_providers'
+ key = 'service_provider'
+ base_url = 'OS-FEDERATION'
+
+ def _build_url_and_put(self, **kwargs):
+ url = self.build_url(dict_args_in_out=kwargs)
+ body = {self.key: kwargs}
+ return self._update(url, body=body, response_key=self.key,
+ method='PUT')
+
+ @utils.positional.method(0)
+ def create(self, id, **kwargs):
+ """Create Service Provider object.
+
+ Utilize Keystone URI:
+ ``PUT /OS-FEDERATION/service_providers/{id}``
+
+ :param id: unique id of the service provider.
+
+ """
+ return self._build_url_and_put(service_provider_id=id,
+ **kwargs)
+
+ def get(self, service_provider):
+ """Fetch Service Provider object
+
+ Utilize Keystone URI:
+ ``GET /OS-FEDERATION/service_providers/{id}``
+
+ :param service_provider: an object with service_provider_id
+ stored inside.
+
+ """
+ return super(ServiceProviderManager, self).get(
+ service_provider_id=base.getid(service_provider))
+
+ def list(self, **kwargs):
+ """List all Service Providers.
+
+ Utilize Keystone URI:
+ ``GET /OS-FEDERATION/service_providers``
+
+ """
+ return super(ServiceProviderManager, self).list(**kwargs)
+
+ def update(self, service_provider, **kwargs):
+ """Update the existing Service Provider object on the server.
+
+ Only properties provided to the function are being updated.
+
+ Utilize Keystone URI:
+ ``PATCH /OS-FEDERATION/service_providers/{id}``
+
+ :param service_provider: an object with service_provider_id
+ stored inside.
+
+ """
+ return super(ServiceProviderManager, self).update(
+ service_provider_id=base.getid(service_provider), **kwargs)
+
+ def delete(self, service_provider):
+ """Delete Service Provider object.
+
+ Utilize Keystone URI:
+ ``DELETE /OS-FEDERATION/service_providers/{id}``
+
+ :param service_provider: an object with service_provider_id
+ stored inside.
+
+ """
+ return super(ServiceProviderManager, self).delete(
+ service_provider_id=base.getid(service_provider))
diff --git a/keystoneclient/v3/contrib/simple_cert.py b/keystoneclient/v3/contrib/simple_cert.py
new file mode 100644
index 0000000..c76234a
--- /dev/null
+++ b/keystoneclient/v3/contrib/simple_cert.py
@@ -0,0 +1,43 @@
+# Copyright 2014 IBM Corp.
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+class SimpleCertManager(object):
+ """Manager for the OS-SIMPLE-CERT extension."""
+
+ def __init__(self, client):
+ self._client = client
+
+ def get_ca_certificates(self):
+ """Get CA certificates.
+
+ :returns: PEM-formatted string.
+ :rtype: str
+
+ """
+
+ resp, body = self._client.get('/OS-SIMPLE-CERT/ca',
+ authenticated=False)
+ return resp.text
+
+ def get_certificates(self):
+ """Get signing certificates.
+
+ :returns: PEM-formatted string.
+ :rtype: str
+
+ """
+
+ resp, body = self._client.get('/OS-SIMPLE-CERT/certificates',
+ authenticated=False)
+ return resp.text
diff --git a/keystoneclient/v3/projects.py b/keystoneclient/v3/projects.py
index 0a98991..5daaee5 100644
--- a/keystoneclient/v3/projects.py
+++ b/keystoneclient/v3/projects.py
@@ -15,6 +15,8 @@
# under the License.
from keystoneclient import base
+from keystoneclient import exceptions
+from keystoneclient.i18n import _
from keystoneclient import utils
@@ -103,8 +105,23 @@ class ProjectManager(base.CrudManager):
fallback_to_auth=True,
**kwargs)
+ def _check_not_parents_as_ids_and_parents_as_list(self, parents_as_ids,
+ parents_as_list):
+ if parents_as_ids and parents_as_list:
+ msg = _('Specify either parents_as_ids or parents_as_list '
+ 'parameters, not both')
+ raise exceptions.ValidationError(msg)
+
+ def _check_not_subtree_as_ids_and_subtree_as_list(self, subtree_as_ids,
+ subtree_as_list):
+ if subtree_as_ids and subtree_as_list:
+ msg = _('Specify either subtree_as_ids or subtree_as_list '
+ 'parameters, not both')
+ raise exceptions.ValidationError(msg)
+
@utils.positional()
- def get(self, project, subtree_as_list=False, parents_as_list=False):
+ def get(self, project, subtree_as_list=False, parents_as_list=False,
+ subtree_as_ids=False, parents_as_ids=False):
"""Get a project.
:param project: project to be retrieved.
@@ -115,17 +132,37 @@ class ProjectManager(base.CrudManager):
:param boolean parents_as_list: retrieve projects above this project
in the hierarchy as a flat list.
(optional)
+ :param boolean subtree_as_ids: retrieve the IDs from the projects below
+ this project in the hierarchy as a
+ structured dictionary. (optional)
+ :param boolean parents_as_ids: retrieve the IDs from the projects above
+ this project in the hierarchy as a
+ structured dictionary. (optional)
+
+ :raises keystoneclient.exceptions.ValidationError: if subtree_as_list
+ and subtree_as_ids or parents_as_list and parents_as_ids are
+ included at the same time in the call.
"""
+ self._check_not_parents_as_ids_and_parents_as_list(
+ parents_as_ids, parents_as_list)
+ self._check_not_subtree_as_ids_and_subtree_as_list(
+ subtree_as_ids, subtree_as_list)
+
# According to the API spec, the query params are key only
- query = ''
+ query_params = []
if subtree_as_list:
- query = '?subtree_as_list'
+ query_params.append('subtree_as_list')
+ if subtree_as_ids:
+ query_params.append('subtree_as_ids')
if parents_as_list:
- query = query + '&parents_as_list' if query else '?parents_as_list'
+ query_params.append('parents_as_list')
+ if parents_as_ids:
+ query_params.append('parents_as_ids')
+ query = self.build_key_only_query(query_params)
dict_args = {'project_id': base.getid(project)}
- url = self.build_url(dict_args_in_out=dict_args) + query
- return self._get(url, self.key)
+ url = self.build_url(dict_args_in_out=dict_args)
+ return self._get(url + query, self.key)
@utils.positional(enforcement=utils.positional.WARN)
def update(self, project, name=None, domain=None, description=None,
diff --git a/keystoneclient/v3/role_assignments.py b/keystoneclient/v3/role_assignments.py
index 6518e43..d71f9eb 100644
--- a/keystoneclient/v3/role_assignments.py
+++ b/keystoneclient/v3/role_assignments.py
@@ -47,7 +47,7 @@ class RoleAssignmentManager(base.CrudManager):
raise exceptions.ValidationError(msg)
def list(self, user=None, group=None, project=None, domain=None, role=None,
- effective=False):
+ effective=False, os_inherit_extension_inherited_to=None):
"""Lists role assignments.
If no arguments are provided, all role assignments in the
@@ -66,6 +66,9 @@ class RoleAssignmentManager(base.CrudManager):
:param role: Role to be used as query filter. (optional)
:param boolean effective: return effective role
assignments. (optional)
+ :param string os_inherit_extension_inherited_to:
+ return inherited role assignments for either 'projects' or
+ 'domains'. (optional)
"""
self._check_not_user_and_group(user, group)
@@ -84,6 +87,9 @@ class RoleAssignmentManager(base.CrudManager):
query_params['role.id'] = base.getid(role)
if effective:
query_params['effective'] = effective
+ if os_inherit_extension_inherited_to:
+ query_params['scope.OS-INHERIT:inherited_to'] = (
+ os_inherit_extension_inherited_to)
return super(RoleAssignmentManager, self).list(**query_params)
diff --git a/requirements.txt b/requirements.txt
index 79887e2..e20190a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,11 +8,11 @@ argparse
Babel>=1.3
iso8601>=0.1.9
netaddr>=0.7.12
-oslo.config>=1.6.0 # Apache-2.0
-oslo.i18n>=1.3.0 # Apache-2.0
-oslo.serialization>=1.2.0 # Apache-2.0
-oslo.utils>=1.2.0 # Apache-2.0
+oslo.config>=1.9.3 # Apache-2.0
+oslo.i18n>=1.5.0 # Apache-2.0
+oslo.serialization>=1.4.0 # Apache-2.0
+oslo.utils>=1.4.0 # Apache-2.0
PrettyTable>=0.7,<0.8
requests>=2.2.0,!=2.4.0
-six>=1.7.0
-stevedore>=1.1.0 # Apache-2.0
+six>=1.9.0
+stevedore>=1.3.0 # Apache-2.0
diff --git a/test-requirements.txt b/test-requirements.txt
index 1bbc10d..0fb4968 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -12,12 +12,12 @@ lxml>=2.3
mock>=1.0
mox3>=0.7.0
oauthlib>=0.6
-oslosphinx>=2.2.0 # Apache-2.0
-oslotest>=1.2.0 # Apache-2.0
+oslosphinx>=2.5.0 # Apache-2.0
+oslotest>=1.5.1 # Apache-2.0
pycrypto>=2.6
-requests-mock>=0.5.1 # Apache-2.0
+requests-mock>=0.6.0 # Apache-2.0
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
-tempest-lib>=0.2.0
+tempest-lib>=0.4.0
testrepository>=0.0.18
testresources>=0.2.4
testtools>=0.9.36,!=1.2.0