diff options
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 |