summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHenry Nash <henryn@linux.vnet.ibm.com>2012-11-12 19:40:21 +0000
committerHenry Nash <henryn@linux.vnet.ibm.com>2012-11-12 19:40:21 +0000
commit7920899af119d1697c333d202ca3272f167c19b0 (patch)
tree2193f585d858e23bfce292f0e7e21c1598ec5b65
parentf617b09874046a0f56f6342638cc8444ed719022 (diff)
downloadpython-keystoneclient-7920899af119d1697c333d202ca3272f167c19b0.tar.gz
Add auth-token code to keystoneclient, along with supporting files
This step in the process duplicates the auth-token code to keystoneclient but, for the moment, leaves a copy in its origional location in keystone. Testing for auth-token is also copied across, as is the cms support file. Although no other project will yet pick up the code here in the client, since the paste.ini files haev not yet been updated, it would work if anyone did reference it. Once the client code is in, the next step is to update all the other project paste files, and then finally retire the code from keystone. Change-Id: I88853a373d406020d54b61cba5a5e887380e3b3e
-rw-r--r--examples/pki/certs/cacert.pem18
-rw-r--r--examples/pki/certs/middleware.pem33
-rw-r--r--examples/pki/certs/signing_cert.pem17
-rw-r--r--examples/pki/certs/ssl_cert.pem17
-rw-r--r--examples/pki/cms/auth_token_revoked.json1
-rw-r--r--examples/pki/cms/auth_token_revoked.pem42
-rw-r--r--examples/pki/cms/auth_token_scoped.json1
-rw-r--r--examples/pki/cms/auth_token_scoped.pem41
-rw-r--r--examples/pki/cms/auth_token_unscoped.json1
-rw-r--r--examples/pki/cms/auth_token_unscoped.pem17
-rw-r--r--examples/pki/cms/revocation_list.json1
-rw-r--r--examples/pki/cms/revocation_list.pem12
-rwxr-xr-xexamples/pki/gen_pki.sh222
-rw-r--r--examples/pki/private/cakey.pem16
-rw-r--r--examples/pki/private/signing_key.pem16
-rw-r--r--examples/pki/private/ssl_key.pem16
-rw-r--r--keystoneclient/common/cms.py169
-rw-r--r--keystoneclient/middleware/auth_token.py854
-rw-r--r--keystoneclient/middleware/test.py67
-rw-r--r--keystoneclient/utils.py7
-rw-r--r--tests/test_auth_token_middleware.py667
-rw-r--r--tools/test-requires2
22 files changed, 2237 insertions, 0 deletions
diff --git a/examples/pki/certs/cacert.pem b/examples/pki/certs/cacert.pem
new file mode 100644
index 0000000..8cf663c
--- /dev/null
+++ b/examples/pki/certs/cacert.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC0TCCAjqgAwIBAgIJAP2TNFqmE1KUMA0GCSqGSIb3DQEBBQUAMIGeMQowCAYD
+VQQFEwE1MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1bm55
+dmFsZTESMBAGA1UEChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTElMCMG
+CSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxMLU2Vs
+ZiBTaWduZWQwIBcNMTIxMTExMTA1NDA2WhgPMjA3MTA1MDYxMDU0MDZaMIGeMQow
+CAYDVQQFEwE1MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1
+bm55dmFsZTESMBAGA1UEChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTEl
+MCMGCSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxML
+U2VsZiBTaWduZWQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMXgnd5wlHAp
+GxZ58LrpEkHU995lT9PxtMgkp0tpFhg7R5HQw9K7TfQk5NHB28hNzf8UE/c0z2pJ
+XggPnAzvdx27NQeJGX5CWsi6fITZ8vH/+SxgfxxC+CE/6BkDpzw21MgBtq11vWL7
+XVaxNeU12Ax889U66i3CrObuCYt2mbpzAgMBAAGjEzARMA8GA1UdEwEB/wQFMAMB
+Af8wDQYJKoZIhvcNAQEFBQADgYEAkFIbnr2/0/XWp+f80Gl6GAC7tdmZFlT9udVF
+q794rXyMlYY64pq34SzfQAn+4DztT4B9yzrTx03tLNr6Uf+5TS+ubcwG41UBBMs/
+Icf9zBMRqr+IXhijS49gQ7dPjqNTCqX+6ILbRWjdXP15ZWymI3ayQL/CMwFt/E+0
+kT6MLes=
+-----END CERTIFICATE-----
diff --git a/examples/pki/certs/middleware.pem b/examples/pki/certs/middleware.pem
new file mode 100644
index 0000000..ae5b8db
--- /dev/null
+++ b/examples/pki/certs/middleware.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIICoTCCAgoCARAwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK
+EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr
+ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x
+MjExMTExMDU0MDZaGA8yMDcxMDUwNjEwNTQwNlowgZAxCzAJBgNVBAYTAlVTMQsw
+CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh
+Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv
+cGVuc3RhY2sub3JnMRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEB
+BQADgY0AMIGJAoGBALVu4bjaOH33yAx0WdpEqj4UDVsLxVjWxEpIbOlDlc6IfJd+
+cUriQtxf6ahjxtzLPERS81SnwZmrICWZngbOn733pULMTZktTJH+o7C74NdKwUSN
+xjlCeWUy+FqIQoje4ygoJRPpMdkp1wHNO0ZERwRN9e8M5TIlx/LRtk+q8bT5AgMB
+AAEwDQYJKoZIhvcNAQEFBQADgYEAcp9ancue9Oq+MkaPucCrIqFhiUsdUThulJlB
+etPpUDGgStBSHgze/oxG2+flIjRoI6gG9Chfw//vWHOwDT7N32AHSgaI4b8/k/+s
+hAV2khYkV4PW2oS1TfeU/vxQzXbgApqhLBNqfFmJVW48aGAr/aqsJi3MYWN3269+
+6vChaVw=
+-----END CERTIFICATE-----
+-----BEGIN PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALVu4bjaOH33yAx0
+WdpEqj4UDVsLxVjWxEpIbOlDlc6IfJd+cUriQtxf6ahjxtzLPERS81SnwZmrICWZ
+ngbOn733pULMTZktTJH+o7C74NdKwUSNxjlCeWUy+FqIQoje4ygoJRPpMdkp1wHN
+O0ZERwRN9e8M5TIlx/LRtk+q8bT5AgMBAAECgYAmwq6EYFJrTvE0//JmN/8qzfvg
+dI5PoWpD+F8UInUxr2T2tHOdrOLd07vGVrKYXu7cJeCIOGKa4r02azAggioL/nE9
+FgPpqEC+QROvLuhFsk1gLZ2pGQ06sveKZVMH22h59BKZkYlhjh5qd4vlmhPqkmPp
+gdXj7ZjDCJhhQdFVkQJBANp18k2mVksn8q29LMieVTSIZNN3ucDA1QHbim+3fp/O
+GxCzU7Mv1Xfnu1zoRFu5/sF3YG0Zy3TGPDrEljBC3rUCQQDUnBjVFXL35OkBZqXW
+taJPzGbsPoqAO+Ls2juS97zNzeGxUNhvcKuEvHO63PXqDxp1535DpvJEBN1rT2FF
+iaO1AkEAt/QTWWFUTqrPxY6DNFdm5fpn9E1fg7icZJkKBDJeFJCH59MpCryfovzl
+n0ERtq9ynlQ4RQYwdR8rvkylLvRP9QJAOiXHFOAc5XeR0nREfwiGL9TzgUFJl/DJ
+C4ZULMnctVzNkTVPPItQHal87WppR26CCiUZ/161e6zo8eRv8hjG0QJABWqfYQuK
+dWH8nxlXS+NFUDbsCdL+XpOVE7iEH7hvSw/A/kz40mLx8sDp/Fz1ysrogR/L+NGC
+Vrlwm4q/WYJO0Q==
+-----END PRIVATE KEY-----
diff --git a/examples/pki/certs/signing_cert.pem b/examples/pki/certs/signing_cert.pem
new file mode 100644
index 0000000..1491b55
--- /dev/null
+++ b/examples/pki/certs/signing_cert.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICoDCCAgkCAREwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK
+EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr
+ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x
+MjExMTExMDU0MDZaGA8yMDcxMDUwNjEwNTQwNlowgY8xCzAJBgNVBAYTAlVTMQsw
+CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh
+Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv
+cGVuc3RhY2sub3JnMREwDwYDVQQDEwhLZXlzdG9uZTCBnzANBgkqhkiG9w0BAQEF
+AAOBjQAwgYkCgYEAuoQC6IBqMxC5845c/ZkLsdcQbTHqIpYJHEkwEoxyeEjwiGFf
+iZmiZ91pSFNc9MfjdJnN+be/ndVS19w1nrrJvV/udVsf6JZWkTPX5HyxnllwznCH
+pP7gfvMZzGsqzWlSdiD6mcRbCYRX9hCCauG3jhCtISINCVYMYQGH6QSib9sCAwEA
+ATANBgkqhkiG9w0BAQUFAAOBgQBCssELi+1RSjEmzeqSnpgUqmtpvB9oxbcwl+xH
+rIrYvqMU6pV2aSxgLDqpGjjusLHUau9Bmu3Myc/fm9/mlPUQHNj0AWl8vvfSlq1b
+vsWMUa1h4UFlPWoF2DIUFd+noBxe5CbcLUV6K0oyJAcPO433OyuGl5oQkhxmoy1J
+w59KRg==
+-----END CERTIFICATE-----
diff --git a/examples/pki/certs/ssl_cert.pem b/examples/pki/certs/ssl_cert.pem
new file mode 100644
index 0000000..0a0bc21
--- /dev/null
+++ b/examples/pki/certs/ssl_cert.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICoTCCAgoCARAwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQK
+EwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZr
+ZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0x
+MjExMTExMDU0MDZaGA8yMDcxMDUwNjEwNTQwNlowgZAxCzAJBgNVBAYTAlVTMQsw
+CQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3Rh
+Y2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBv
+cGVuc3RhY2sub3JnMRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEB
+BQADgY0AMIGJAoGBALVu4bjaOH33yAx0WdpEqj4UDVsLxVjWxEpIbOlDlc6IfJd+
+cUriQtxf6ahjxtzLPERS81SnwZmrICWZngbOn733pULMTZktTJH+o7C74NdKwUSN
+xjlCeWUy+FqIQoje4ygoJRPpMdkp1wHNO0ZERwRN9e8M5TIlx/LRtk+q8bT5AgMB
+AAEwDQYJKoZIhvcNAQEFBQADgYEAcp9ancue9Oq+MkaPucCrIqFhiUsdUThulJlB
+etPpUDGgStBSHgze/oxG2+flIjRoI6gG9Chfw//vWHOwDT7N32AHSgaI4b8/k/+s
+hAV2khYkV4PW2oS1TfeU/vxQzXbgApqhLBNqfFmJVW48aGAr/aqsJi3MYWN3269+
+6vChaVw=
+-----END CERTIFICATE-----
diff --git a/examples/pki/cms/auth_token_revoked.json b/examples/pki/cms/auth_token_revoked.json
new file mode 100644
index 0000000..92c6922
--- /dev/null
+++ b/examples/pki/cms/auth_token_revoked.json
@@ -0,0 +1 @@
+{"access": {"serviceCatalog": [{"endpoints": [{"adminURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", "region": "regionOne", "internalURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", "publicURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a"}], "endpoints_links": [], "type": "volume", "name": "volume"}, {"endpoints": [{"adminURL": "http://127.0.0.1:9292/v1", "region": "regionOne", "internalURL": "http://127.0.0.1:9292/v1", "publicURL": "http://127.0.0.1:9292/v1"}], "endpoints_links": [], "type": "image", "name": "glance"}, {"endpoints": [{"adminURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", "region": "regionOne", "internalURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", "publicURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a"}], "endpoints_links": [], "type": "compute", "name": "nova"}, {"endpoints": [{"adminURL": "http://127.0.0.1:35357/v2.0", "region": "RegionOne", "internalURL": "http://127.0.0.1:35357/v2.0", "publicURL": "http://127.0.0.1:5000/v2.0"}], "endpoints_links": [], "type": "identity", "name": "keystone"}],"token": {"expires": "2012-06-02T14:47:34Z", "id": "placeholder", "tenant": {"enabled": true, "description": null, "name": "tenant_name1", "id": "tenant_id1"}}, "user": {"username": "revoked_username1", "roles_links": ["role1","role2"], "id": "revoked_user_id1", "roles": [{"name": "role1"}, {"name": "role2"}], "name": "revoked_username1"}}}
diff --git a/examples/pki/cms/auth_token_revoked.pem b/examples/pki/cms/auth_token_revoked.pem
new file mode 100644
index 0000000..0bd7a70
--- /dev/null
+++ b/examples/pki/cms/auth_token_revoked.pem
@@ -0,0 +1,42 @@
+-----BEGIN CMS-----
+MIIHVgYJKoZIhvcNAQcCoIIHRzCCB0MCAQExCTAHBgUrDgMCGjCCBeQGCSqGSIb3
+DQEHAaCCBdUEggXReyJhY2Nlc3MiOiB7InNlcnZpY2VDYXRhbG9nIjogW3siZW5k
+cG9pbnRzIjogW3siYWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2L3Yx
+LzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwgInJlZ2lvbiI6ICJy
+ZWdpb25PbmUiLCAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2
+L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwgInB1YmxpY1VS
+TCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzYvdjEvNjRiNmYzZmJjYzUzNDM1ZThh
+NjBmY2Y4OWJiNjYxN2EifV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUi
+OiAidm9sdW1lIiwgIm5hbWUiOiAidm9sdW1lIn0sIHsiZW5kcG9pbnRzIjogW3si
+YWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5MjkyL3YxIiwgInJlZ2lvbiI6
+ICJyZWdpb25PbmUiLCAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5
+MjkyL3YxIiwgInB1YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjkyOTIvdjEi
+fV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUiOiAiaW1hZ2UiLCAibmFt
+ZSI6ICJnbGFuY2UifSwgeyJlbmRwb2ludHMiOiBbeyJhZG1pblVSTCI6ICJodHRw
+Oi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5
+YmI2NjE3YSIsICJyZWdpb24iOiAicmVnaW9uT25lIiwgImludGVybmFsVVJMIjog
+Imh0dHA6Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQzNWU4YTYw
+ZmNmODliYjY2MTdhIiwgInB1YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3
+NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSJ9XSwgImVu
+ZHBvaW50c19saW5rcyI6IFtdLCAidHlwZSI6ICJjb21wdXRlIiwgIm5hbWUiOiAi
+bm92YSJ9LCB7ImVuZHBvaW50cyI6IFt7ImFkbWluVVJMIjogImh0dHA6Ly8xMjcu
+MC4wLjE6MzUzNTcvdjIuMCIsICJyZWdpb24iOiAiUmVnaW9uT25lIiwgImludGVy
+bmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6MzUzNTcvdjIuMCIsICJwdWJsaWNV
+UkwiOiAiaHR0cDovLzEyNy4wLjAuMTo1MDAwL3YyLjAifV0sICJlbmRwb2ludHNf
+bGlua3MiOiBbXSwgInR5cGUiOiAiaWRlbnRpdHkiLCAibmFtZSI6ICJrZXlzdG9u
+ZSJ9XSwidG9rZW4iOiB7ImV4cGlyZXMiOiAiMjAxMi0wNi0wMlQxNDo0NzozNFoi
+LCAiaWQiOiAicGxhY2Vob2xkZXIiLCAidGVuYW50IjogeyJlbmFibGVkIjogdHJ1
+ZSwgImRlc2NyaXB0aW9uIjogbnVsbCwgIm5hbWUiOiAidGVuYW50X25hbWUxIiwg
+ImlkIjogInRlbmFudF9pZDEifX0sICJ1c2VyIjogeyJ1c2VybmFtZSI6ICJyZXZv
+a2VkX3VzZXJuYW1lMSIsICJyb2xlc19saW5rcyI6IFsicm9sZTEiLCJyb2xlMiJd
+LCAiaWQiOiAicmV2b2tlZF91c2VyX2lkMSIsICJyb2xlcyI6IFt7Im5hbWUiOiAi
+cm9sZTEifSwgeyJuYW1lIjogInJvbGUyIn1dLCAibmFtZSI6ICJyZXZva2VkX3Vz
+ZXJuYW1lMSJ9fX0NCjGCAUkwggFFAgEBMIGkMIGeMQowCAYDVQQFEwE1MQswCQYD
+VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1bm55dmFsZTESMBAGA1UE
+ChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTElMCMGCSqGSIb3DQEJARYW
+a2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxMLU2VsZiBTaWduZWQCAREw
+BwYFKw4DAhowDQYJKoZIhvcNAQEBBQAEgYBhV5KrVjcdACPUNafkPY+lgCSlh6uc
+N55SATBcQmg1/argEUFg/cx2GcF7ftQV384iGepLEgsq+6om2wPw6DWA0RknpVLJ
+vMsHbWdGoXIZ5jRuAQTPtkXcJQOR677baDHvGJ+5zwBBDT2CmN2Tcv348+Xpjp7D
+hF/cmAXnYYo00g==
+-----END CMS-----
diff --git a/examples/pki/cms/auth_token_scoped.json b/examples/pki/cms/auth_token_scoped.json
new file mode 100644
index 0000000..16eb644
--- /dev/null
+++ b/examples/pki/cms/auth_token_scoped.json
@@ -0,0 +1 @@
+{"access": {"serviceCatalog": [{"endpoints": [{"adminURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", "region": "regionOne", "internalURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a", "publicURL": "http://127.0.0.1:8776/v1/64b6f3fbcc53435e8a60fcf89bb6617a"}], "endpoints_links": [], "type": "volume", "name": "volume"}, {"endpoints": [{"adminURL": "http://127.0.0.1:9292/v1", "region": "regionOne", "internalURL": "http://127.0.0.1:9292/v1", "publicURL": "http://127.0.0.1:9292/v1"}], "endpoints_links": [], "type": "image", "name": "glance"}, {"endpoints": [{"adminURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", "region": "regionOne", "internalURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a", "publicURL": "http://127.0.0.1:8774/v1.1/64b6f3fbcc53435e8a60fcf89bb6617a"}], "endpoints_links": [], "type": "compute", "name": "nova"}, {"endpoints": [{"adminURL": "http://127.0.0.1:35357/v2.0", "region": "RegionOne", "internalURL": "http://127.0.0.1:35357/v2.0", "publicURL": "http://127.0.0.1:5000/v2.0"}], "endpoints_links": [], "type": "identity", "name": "keystone"}],"token": {"expires": "2012-06-02T14:47:34Z", "id": "placeholder", "tenant": {"enabled": true, "description": null, "name": "tenant_name1", "id": "tenant_id1"}}, "user": {"username": "user_name1", "roles_links": ["role1","role2"], "id": "user_id1", "roles": [{"name": "role1"}, {"name": "role2"}], "name": "user_name1"}}}
diff --git a/examples/pki/cms/auth_token_scoped.pem b/examples/pki/cms/auth_token_scoped.pem
new file mode 100644
index 0000000..529d7f8
--- /dev/null
+++ b/examples/pki/cms/auth_token_scoped.pem
@@ -0,0 +1,41 @@
+-----BEGIN CMS-----
+MIIHQAYJKoZIhvcNAQcCoIIHMTCCBy0CAQExCTAHBgUrDgMCGjCCBc4GCSqGSIb3
+DQEHAaCCBb8EggW7eyJhY2Nlc3MiOiB7InNlcnZpY2VDYXRhbG9nIjogW3siZW5k
+cG9pbnRzIjogW3siYWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2L3Yx
+LzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwgInJlZ2lvbiI6ICJy
+ZWdpb25PbmUiLCAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo4Nzc2
+L3YxLzY0YjZmM2ZiY2M1MzQzNWU4YTYwZmNmODliYjY2MTdhIiwgInB1YmxpY1VS
+TCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3NzYvdjEvNjRiNmYzZmJjYzUzNDM1ZThh
+NjBmY2Y4OWJiNjYxN2EifV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUi
+OiAidm9sdW1lIiwgIm5hbWUiOiAidm9sdW1lIn0sIHsiZW5kcG9pbnRzIjogW3si
+YWRtaW5VUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5MjkyL3YxIiwgInJlZ2lvbiI6
+ICJyZWdpb25PbmUiLCAiaW50ZXJuYWxVUkwiOiAiaHR0cDovLzEyNy4wLjAuMTo5
+MjkyL3YxIiwgInB1YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjkyOTIvdjEi
+fV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUiOiAiaW1hZ2UiLCAibmFt
+ZSI6ICJnbGFuY2UifSwgeyJlbmRwb2ludHMiOiBbeyJhZG1pblVSTCI6ICJodHRw
+Oi8vMTI3LjAuMC4xOjg3NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5
+YmI2NjE3YSIsICJyZWdpb24iOiAicmVnaW9uT25lIiwgImludGVybmFsVVJMIjog
+Imh0dHA6Ly8xMjcuMC4wLjE6ODc3NC92MS4xLzY0YjZmM2ZiY2M1MzQzNWU4YTYw
+ZmNmODliYjY2MTdhIiwgInB1YmxpY1VSTCI6ICJodHRwOi8vMTI3LjAuMC4xOjg3
+NzQvdjEuMS82NGI2ZjNmYmNjNTM0MzVlOGE2MGZjZjg5YmI2NjE3YSJ9XSwgImVu
+ZHBvaW50c19saW5rcyI6IFtdLCAidHlwZSI6ICJjb21wdXRlIiwgIm5hbWUiOiAi
+bm92YSJ9LCB7ImVuZHBvaW50cyI6IFt7ImFkbWluVVJMIjogImh0dHA6Ly8xMjcu
+MC4wLjE6MzUzNTcvdjIuMCIsICJyZWdpb24iOiAiUmVnaW9uT25lIiwgImludGVy
+bmFsVVJMIjogImh0dHA6Ly8xMjcuMC4wLjE6MzUzNTcvdjIuMCIsICJwdWJsaWNV
+UkwiOiAiaHR0cDovLzEyNy4wLjAuMTo1MDAwL3YyLjAifV0sICJlbmRwb2ludHNf
+bGlua3MiOiBbXSwgInR5cGUiOiAiaWRlbnRpdHkiLCAibmFtZSI6ICJrZXlzdG9u
+ZSJ9XSwidG9rZW4iOiB7ImV4cGlyZXMiOiAiMjAxMi0wNi0wMlQxNDo0NzozNFoi
+LCAiaWQiOiAicGxhY2Vob2xkZXIiLCAidGVuYW50IjogeyJlbmFibGVkIjogdHJ1
+ZSwgImRlc2NyaXB0aW9uIjogbnVsbCwgIm5hbWUiOiAidGVuYW50X25hbWUxIiwg
+ImlkIjogInRlbmFudF9pZDEifX0sICJ1c2VyIjogeyJ1c2VybmFtZSI6ICJ1c2Vy
+X25hbWUxIiwgInJvbGVzX2xpbmtzIjogWyJyb2xlMSIsInJvbGUyIl0sICJpZCI6
+ICJ1c2VyX2lkMSIsICJyb2xlcyI6IFt7Im5hbWUiOiAicm9sZTEifSwgeyJuYW1l
+IjogInJvbGUyIn1dLCAibmFtZSI6ICJ1c2VyX25hbWUxIn19fQ0KMYIBSTCCAUUC
+AQEwgaQwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTES
+MBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3RhY2sxETAPBgNVBAsT
+CEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVuc3RhY2sub3Jn
+MRQwEgYDVQQDEwtTZWxmIFNpZ25lZAIBETAHBgUrDgMCGjANBgkqhkiG9w0BAQEF
+AASBgFizBVs3dCvlHx04nUHgXHpaA9RL+e3uaaNszK9UwCBpBlv8c6+74sz6i3+G
+eYDIpL9bc6QgNJ6cKhmW5yLmS8/+mmAMAcm06bdWc7p/mqC3Ild+xmQ+OHDYyyJg
+DvtRUgtidFUCvxne/nwKK0WHJlpY+iwWqel5F+Xqmb8vheb1
+-----END CMS-----
diff --git a/examples/pki/cms/auth_token_unscoped.json b/examples/pki/cms/auth_token_unscoped.json
new file mode 100644
index 0000000..b2340a7
--- /dev/null
+++ b/examples/pki/cms/auth_token_unscoped.json
@@ -0,0 +1 @@
+{"access": {"token": {"expires": "2012-08-17T15:35:34Z", "id": "01e032c996ef4406b144335915a41e79"}, "serviceCatalog": {}, "user": {"username": "user_name1", "roles_links": [], "id": "c9c89e3be3ee453fbf00c7966f6d3fbd", "roles": [{'name': 'role1'},{'name': 'role2'},], "name": "user_name1"}}} \ No newline at end of file
diff --git a/examples/pki/cms/auth_token_unscoped.pem b/examples/pki/cms/auth_token_unscoped.pem
new file mode 100644
index 0000000..d42d1f1
--- /dev/null
+++ b/examples/pki/cms/auth_token_unscoped.pem
@@ -0,0 +1,17 @@
+-----BEGIN CMS-----
+MIICpwYJKoZIhvcNAQcCoIICmDCCApQCAQExCTAHBgUrDgMCGjCCATUGCSqGSIb3
+DQEHAaCCASYEggEieyJhY2Nlc3MiOiB7InRva2VuIjogeyJleHBpcmVzIjogIjIw
+MTItMDgtMTdUMTU6MzU6MzRaIiwgImlkIjogIjAxZTAzMmM5OTZlZjQ0MDZiMTQ0
+MzM1OTE1YTQxZTc5In0sICJzZXJ2aWNlQ2F0YWxvZyI6IHt9LCAidXNlciI6IHsi
+dXNlcm5hbWUiOiAidXNlcl9uYW1lMSIsICJyb2xlc19saW5rcyI6IFtdLCAiaWQi
+OiAiYzljODllM2JlM2VlNDUzZmJmMDBjNzk2NmY2ZDNmYmQiLCAicm9sZXMiOiBb
+eyduYW1lJzogJ3JvbGUxJ30seyduYW1lJzogJ3JvbGUyJ30sXSwgIm5hbWUiOiAi
+dXNlcl9uYW1lMSJ9fX0xggFJMIIBRQIBATCBpDCBnjEKMAgGA1UEBRMBNTELMAkG
+A1UEBhMCVVMxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlTdW5ueXZhbGUxEjAQBgNV
+BAoTCU9wZW5TdGFjazERMA8GA1UECxMIS2V5c3RvbmUxJTAjBgkqhkiG9w0BCQEW
+FmtleXN0b25lQG9wZW5zdGFjay5vcmcxFDASBgNVBAMTC1NlbGYgU2lnbmVkAgER
+MAcGBSsOAwIaMA0GCSqGSIb3DQEBAQUABIGAITCwkW7cAbcWbCBD5GfGMGHB9hP/
+UagaCZ8HFhlzjdQoJjvC+Mtu+3lWlwqPGR8ztY9kBc1401S2qJxD4FGo+M3CkNpF
+s0mtaT2PUJfFkDCzHqeBQNFHyZeqLjkPYnokPcw4s3i60DBGTFfAiUT3xumn8a4h
+C+zEAee35C/A+Iw=
+-----END CMS-----
diff --git a/examples/pki/cms/revocation_list.json b/examples/pki/cms/revocation_list.json
new file mode 100644
index 0000000..c3401b0
--- /dev/null
+++ b/examples/pki/cms/revocation_list.json
@@ -0,0 +1 @@
+{"revoked":[{"id":"7acfcfdaf6a14aebe97c61c5947bc4d3","expires":"2012-08-14T17:58:48Z"}]}
diff --git a/examples/pki/cms/revocation_list.pem b/examples/pki/cms/revocation_list.pem
new file mode 100644
index 0000000..969d10d
--- /dev/null
+++ b/examples/pki/cms/revocation_list.pem
@@ -0,0 +1,12 @@
+-----BEGIN CMS-----
+MIIB2QYJKoZIhvcNAQcCoIIByjCCAcYCAQExCTAHBgUrDgMCGjBpBgkqhkiG9w0B
+BwGgXARaeyJyZXZva2VkIjpbeyJpZCI6IjdhY2ZjZmRhZjZhMTRhZWJlOTdjNjFj
+NTk0N2JjNGQzIiwiZXhwaXJlcyI6IjIwMTItMDgtMTRUMTc6NTg6NDhaIn1dfQ0K
+MYIBSTCCAUUCAQEwgaQwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQswCQYD
+VQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3RhY2sx
+ETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVu
+c3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZAIBETAHBgUrDgMCGjANBgkq
+hkiG9w0BAQEFAASBgDNDhvViAo8EqTVVvZ00pWUWjajTwoV1w1os1XDJ1XacBUo+
+rsh7gljIIVuvHL2F9C660I5jxhb7QVsTge3CwSiDmexxBAPOs4lNR5hFH7FdT47b
+OK2qd0XnRjo5F7odUxIkozuQ/UISaNTPeWxGEMNVhpTXo2Dwn8wN1wrs/Z2E
+-----END CMS-----
diff --git a/examples/pki/gen_pki.sh b/examples/pki/gen_pki.sh
new file mode 100755
index 0000000..9bf6c32
--- /dev/null
+++ b/examples/pki/gen_pki.sh
@@ -0,0 +1,222 @@
+#!/bin/bash
+
+# Copyright 2012 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+# This script generates the crypto necessary for the SSL tests.
+
+DIR=`dirname "$0"`
+CURRENT_DIR=`cd "$DIR" && pwd`
+CERTS_DIR=$CURRENT_DIR/certs
+PRIVATE_DIR=$CURRENT_DIR/private
+CMS_DIR=$CURRENT_DIR/cms
+
+
+function rm_old {
+ rm -rf $CERTS_DIR/*.pem
+ rm -rf $PRIVATE_DIR/*.pem
+}
+
+function cleanup {
+ rm -rf *.conf > /dev/null 2>&1
+ rm -rf index* > /dev/null 2>&1
+ rm -rf *.crt > /dev/null 2>&1
+ rm -rf newcerts > /dev/null 2>&1
+ rm -rf *.pem > /dev/null 2>&1
+ rm -rf serial* > /dev/null 2>&1
+}
+
+function generate_ca_conf {
+ echo '
+[ req ]
+default_bits = 1024
+default_keyfile = cakey.pem
+default_md = sha1
+
+prompt = no
+distinguished_name = ca_distinguished_name
+
+x509_extensions = ca_extensions
+
+[ ca_distinguished_name ]
+serialNumber = 5
+countryName = US
+stateOrProvinceName = CA
+localityName = Sunnyvale
+organizationName = OpenStack
+organizationalUnitName = Keystone
+emailAddress = keystone@openstack.org
+commonName = Self Signed
+
+[ ca_extensions ]
+basicConstraints = critical,CA:true
+' > ca.conf
+}
+
+function generate_ssl_req_conf {
+ echo '
+[ req ]
+default_bits = 1024
+default_keyfile = keystonekey.pem
+default_md = sha1
+
+prompt = no
+distinguished_name = distinguished_name
+
+[ distinguished_name ]
+countryName = US
+stateOrProvinceName = CA
+localityName = Sunnyvale
+organizationName = OpenStack
+organizationalUnitName = Keystone
+commonName = localhost
+emailAddress = keystone@openstack.org
+' > ssl_req.conf
+}
+
+function generate_cms_signing_req_conf {
+ echo '
+[ req ]
+default_bits = 1024
+default_keyfile = keystonekey.pem
+default_md = sha1
+
+prompt = no
+distinguished_name = distinguished_name
+
+[ distinguished_name ]
+countryName = US
+stateOrProvinceName = CA
+localityName = Sunnyvale
+organizationName = OpenStack
+organizationalUnitName = Keystone
+commonName = Keystone
+emailAddress = keystone@openstack.org
+' > cms_signing_req.conf
+}
+
+function generate_signing_conf {
+ echo '
+[ ca ]
+default_ca = signing_ca
+
+[ signing_ca ]
+dir = .
+database = $dir/index.txt
+new_certs_dir = $dir/newcerts
+
+certificate = $dir/certs/cacert.pem
+serial = $dir/serial
+private_key = $dir/private/cakey.pem
+
+default_days = 21360
+default_crl_days = 30
+default_md = sha1
+
+policy = policy_any
+
+[ policy_any ]
+countryName = supplied
+stateOrProvinceName = supplied
+localityName = optional
+organizationName = supplied
+organizationalUnitName = supplied
+emailAddress = supplied
+commonName = supplied
+' > signing.conf
+}
+
+function setup {
+ touch index.txt
+ echo '10' > serial
+ generate_ca_conf
+ mkdir newcerts
+}
+
+function check_error {
+ if [ $1 != 0 ] ; then
+ echo "Failed! rc=${1}"
+ echo 'Bailing ...'
+ cleanup
+ exit $1
+ else
+ echo 'Done'
+ fi
+}
+
+function generate_ca {
+ echo 'Generating New CA Certificate ...'
+ openssl req -x509 -newkey rsa:1024 -days 21360 -out $CERTS_DIR/cacert.pem -keyout $PRIVATE_DIR/cakey.pem -outform PEM -config ca.conf -nodes
+ check_error $?
+}
+
+function ssl_cert_req {
+ echo 'Generating SSL Certificate Request ...'
+ generate_ssl_req_conf
+ openssl req -newkey rsa:1024 -keyout $PRIVATE_DIR/ssl_key.pem -keyform PEM -out ssl_req.pem -outform PEM -config ssl_req.conf -nodes
+ check_error $?
+ #openssl req -in req.pem -text -noout
+}
+
+function cms_signing_cert_req {
+ echo 'Generating CMS Signing Certificate Request ...'
+ generate_cms_signing_req_conf
+ openssl req -newkey rsa:1024 -keyout $PRIVATE_DIR/signing_key.pem -keyform PEM -out cms_signing_req.pem -outform PEM -config cms_signing_req.conf -nodes
+ check_error $?
+ #openssl req -in req.pem -text -noout
+}
+
+function issue_certs {
+ generate_signing_conf
+ echo 'Issuing SSL Certificate ...'
+ openssl ca -in ssl_req.pem -config signing.conf -batch
+ check_error $?
+ openssl x509 -in $CURRENT_DIR/newcerts/10.pem -out $CERTS_DIR/ssl_cert.pem
+ check_error $?
+ echo 'Issuing CMS Signing Certificate ...'
+ openssl ca -in cms_signing_req.pem -config signing.conf -batch
+ check_error $?
+ openssl x509 -in $CURRENT_DIR/newcerts/11.pem -out $CERTS_DIR/signing_cert.pem
+ check_error $?
+}
+
+function create_middleware_cert {
+ cp $CERTS_DIR/ssl_cert.pem $CERTS_DIR/middleware.pem
+ cat $PRIVATE_DIR/ssl_key.pem >> $CERTS_DIR/middleware.pem
+}
+
+function check_openssl {
+ echo 'Checking openssl availability ...'
+ which openssl
+ check_error $?
+}
+
+function gen_sample_cms {
+ for json_file in "${CMS_DIR}/auth_token_revoked.json" "${CMS_DIR}/auth_token_unscoped.json" "${CMS_DIR}/auth_token_scoped.json" "${CMS_DIR}/revocation_list.json"
+ do
+ openssl cms -sign -in $json_file -nosmimecap -signer $CERTS_DIR/signing_cert.pem -inkey $PRIVATE_DIR/signing_key.pem -outform PEM -nodetach -nocerts -noattr -out ${json_file/.json/.pem}
+ done
+}
+
+check_openssl
+rm_old
+cleanup
+setup
+generate_ca
+ssl_cert_req
+cms_signing_cert_req
+issue_certs
+create_middleware_cert
+gen_sample_cms
+cleanup
diff --git a/examples/pki/private/cakey.pem b/examples/pki/private/cakey.pem
new file mode 100644
index 0000000..3db9c22
--- /dev/null
+++ b/examples/pki/private/cakey.pem
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMXgnd5wlHApGxZ5
+8LrpEkHU995lT9PxtMgkp0tpFhg7R5HQw9K7TfQk5NHB28hNzf8UE/c0z2pJXggP
+nAzvdx27NQeJGX5CWsi6fITZ8vH/+SxgfxxC+CE/6BkDpzw21MgBtq11vWL7XVax
+NeU12Ax889U66i3CrObuCYt2mbpzAgMBAAECgYEAligxJE9CFSrcR14Zc3zSQeqe
+fcFbpnXQveAyo2MHRTQWx2wobY19RjuI+DOn2IRSQbK2w+zrSLiMBonx3U8Kj8nx
+A4EQ75GLJEEr81TvBoIZSJAqrowNrkXNq8W++qwjlGXRjKiBAYlKMrFvR4lij4XN
+6cdB7kGdSIUmhvC20sECQQD4ebCGfsgFWnrqOrco6T9eQRTvP3+gJuqYXYLuVSTC
+R4gHxT5QVXSZt/Hv3UWJ0BLDbyLzLGHf30w1AqgwsUP5AkEAy96qXq6j2+IRa5w7
+2G+KZHF5N/MK/Hyy27Jw67GBVeGQj1Dwq2ZGAJBZrfXjTtQQAGdQ7EfOTCAOzHgX
+2Bx0ywJAYqfGbBBIkL+VEA0SDh9WNrE2g6u9m7P371kplEGgH7dRDmzFShYz/pin
+aep8IrTHzmsBAHY9wiqh0mZkqzim2QJADTYdxkr89WfeByI1wp3f0wiDeXu3j4sp
+MBGNPcjf/8fBTXhKUGEtUiYImbxggaA+dTg8x0MT/FzreJajvO6DJwJARMc6rhzv
+aTlm4IgApcDPBeuz6SKex9TfvDUJpqACoFM4lMgyHADi9NrJBslxFHPP5eTiM2Ag
+vI7EuW837e6raQ==
+-----END PRIVATE KEY-----
diff --git a/examples/pki/private/signing_key.pem b/examples/pki/private/signing_key.pem
new file mode 100644
index 0000000..b6ad710
--- /dev/null
+++ b/examples/pki/private/signing_key.pem
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALqEAuiAajMQufOO
+XP2ZC7HXEG0x6iKWCRxJMBKMcnhI8IhhX4mZomfdaUhTXPTH43SZzfm3v53VUtfc
+NZ66yb1f7nVbH+iWVpEz1+R8sZ5ZcM5wh6T+4H7zGcxrKs1pUnYg+pnEWwmEV/YQ
+gmrht44QrSEiDQlWDGEBh+kEom/bAgMBAAECgYBywfSUHya4gqsW2toGQps6cauu
+s85uN0glujY0w2tO7Pnpv5errvaI12cG1BvWlAIz5MohwlfIgc919wyavCyRJgQN
+xQo5v5MEMYKKc8ppmXpRr03HLwoPLOHVs6UHRJQT9dhOBfmLzMZIP7P/lJlt2/1X
+Okwxft/PWorczKX1aQJBAORlVqP+Cj4r5kz1A77agnCvINioV1VM5n9PvzPVzYLH
+5r1I53RWFooy1Hx2RUCmtSRQMZMeI9iGMg9c8d3LJ4UCQQDRDuIAd3AoNBcwXKC4
+BPNkbI9BSqnpIdZo87BzpY8rJ/ra3VHMHuq4w+gQsmmEy3pp01AZd1uBqv3s1wHy
+muffAkEAn2ZmiH+lUGy9B5q8qXfBL7naF7utb/gCqnnSvO+LxamUTSjTeKsYgg0l
+pVO503xF0fkyEDYp2FUYHQbGOwAtLQJAHkJ3N/YRx9/yU0+0+63LxQdpnNu/yDzb
+mglbywF1vZtl1fQe+NqowuGoX3JTj6McLuElQOpj1lr3siZU49bEJQJBANRazUzj
+Xfoja7wGuZ3PwHdxxoNDlJ2u0rYjcfK9VZuPGSz/25iCOkaar3OralJ3lfCWbFKA
+vvRp8Hl2Yk4hdKM=
+-----END PRIVATE KEY-----
diff --git a/examples/pki/private/ssl_key.pem b/examples/pki/private/ssl_key.pem
new file mode 100644
index 0000000..2e4667f
--- /dev/null
+++ b/examples/pki/private/ssl_key.pem
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALVu4bjaOH33yAx0
+WdpEqj4UDVsLxVjWxEpIbOlDlc6IfJd+cUriQtxf6ahjxtzLPERS81SnwZmrICWZ
+ngbOn733pULMTZktTJH+o7C74NdKwUSNxjlCeWUy+FqIQoje4ygoJRPpMdkp1wHN
+O0ZERwRN9e8M5TIlx/LRtk+q8bT5AgMBAAECgYAmwq6EYFJrTvE0//JmN/8qzfvg
+dI5PoWpD+F8UInUxr2T2tHOdrOLd07vGVrKYXu7cJeCIOGKa4r02azAggioL/nE9
+FgPpqEC+QROvLuhFsk1gLZ2pGQ06sveKZVMH22h59BKZkYlhjh5qd4vlmhPqkmPp
+gdXj7ZjDCJhhQdFVkQJBANp18k2mVksn8q29LMieVTSIZNN3ucDA1QHbim+3fp/O
+GxCzU7Mv1Xfnu1zoRFu5/sF3YG0Zy3TGPDrEljBC3rUCQQDUnBjVFXL35OkBZqXW
+taJPzGbsPoqAO+Ls2juS97zNzeGxUNhvcKuEvHO63PXqDxp1535DpvJEBN1rT2FF
+iaO1AkEAt/QTWWFUTqrPxY6DNFdm5fpn9E1fg7icZJkKBDJeFJCH59MpCryfovzl
+n0ERtq9ynlQ4RQYwdR8rvkylLvRP9QJAOiXHFOAc5XeR0nREfwiGL9TzgUFJl/DJ
+C4ZULMnctVzNkTVPPItQHal87WppR26CCiUZ/161e6zo8eRv8hjG0QJABWqfYQuK
+dWH8nxlXS+NFUDbsCdL+XpOVE7iEH7hvSw/A/kz40mLx8sDp/Fz1ysrogR/L+NGC
+Vrlwm4q/WYJO0Q==
+-----END PRIVATE KEY-----
diff --git a/keystoneclient/common/cms.py b/keystoneclient/common/cms.py
new file mode 100644
index 0000000..93c0505
--- /dev/null
+++ b/keystoneclient/common/cms.py
@@ -0,0 +1,169 @@
+import hashlib
+
+import logging
+
+
+subprocess = None
+LOG = logging.getLogger(__name__)
+PKI_ANS1_PREFIX = 'MII'
+
+
+def _ensure_subprocess():
+ # NOTE(vish): late loading subprocess so we can
+ # use the green version if we are in
+ # eventlet.
+ global subprocess
+ if not subprocess:
+ try:
+ from eventlet import patcher
+ if patcher.already_patched.get('os'):
+ from eventlet.green import subprocess
+ else:
+ import subprocess
+ except ImportError:
+ import subprocess
+
+
+def cms_verify(formatted, signing_cert_file_name, ca_file_name):
+ """
+ verifies the signature of the contents IAW CMS syntax
+ """
+ _ensure_subprocess()
+ process = subprocess.Popen(["openssl", "cms", "-verify",
+ "-certfile", signing_cert_file_name,
+ "-CAfile", ca_file_name,
+ "-inform", "PEM",
+ "-nosmimecap", "-nodetach",
+ "-nocerts", "-noattr"],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ output, err = process.communicate(formatted)
+ retcode = process.poll()
+ if retcode:
+ LOG.error('Verify error: %s' % err)
+ raise subprocess.CalledProcessError(retcode, "openssl", output=err)
+ return output
+
+
+def token_to_cms(signed_text):
+ copy_of_text = signed_text.replace('-', '/')
+
+ formatted = "-----BEGIN CMS-----\n"
+ line_length = 64
+ while len(copy_of_text) > 0:
+ if (len(copy_of_text) > line_length):
+ formatted += copy_of_text[:line_length]
+ copy_of_text = copy_of_text[line_length:]
+ else:
+ formatted += copy_of_text
+ copy_of_text = ""
+ formatted += "\n"
+
+ formatted += "-----END CMS-----\n"
+
+ return formatted
+
+
+def verify_token(token, signing_cert_file_name, ca_file_name):
+ return cms_verify(token_to_cms(token),
+ signing_cert_file_name,
+ ca_file_name)
+
+
+def is_ans1_token(token):
+ '''
+ thx to ayoung for sorting this out.
+
+ base64 decoded hex representation of MII is 3082
+ In [3]: binascii.hexlify(base64.b64decode('MII='))
+ Out[3]: '3082'
+
+ re: http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf
+
+ pg4: For tags from 0 to 30 the first octet is the identfier
+ pg10: Hex 30 means sequence, followed by the length of that sequence.
+ pg5: Second octet is the length octet
+ first bit indicates short or long form, next 7 bits encode the number
+ of subsequent octets that make up the content length octets as an
+ unsigned binary int
+
+ 82 = 10000010 (first bit indicates long form)
+ 0000010 = 2 octets of content length
+ so read the next 2 octets to get the length of the content.
+
+ In the case of a very large content length there could be a requirement to
+ have more than 2 octets to designate the content length, therefore
+ requiring us to check for MIM, MIQ, etc.
+ In [4]: base64.b64encode(binascii.a2b_hex('3083'))
+ Out[4]: 'MIM='
+ In [5]: base64.b64encode(binascii.a2b_hex('3084'))
+ Out[5]: 'MIQ='
+ Checking for MI would become invalid at 16 octets of content length
+ 10010000 = 90
+ In [6]: base64.b64encode(binascii.a2b_hex('3090'))
+ Out[6]: 'MJA='
+ Checking for just M is insufficient
+
+ But we will only check for MII:
+ Max length of the content using 2 octets is 7FFF or 32767
+ It's not practical to support a token of this length or greater in http
+ therefore, we will check for MII only and ignore the case of larger tokens
+ '''
+ return token[:3] == PKI_ANS1_PREFIX
+
+
+def cms_sign_text(text, signing_cert_file_name, signing_key_file_name):
+ """ Uses OpenSSL to sign a document
+ Produces a Base64 encoding of a DER formatted CMS Document
+ http://en.wikipedia.org/wiki/Cryptographic_Message_Syntax
+ """
+ _ensure_subprocess()
+ process = subprocess.Popen(["openssl", "cms", "-sign",
+ "-signer", signing_cert_file_name,
+ "-inkey", signing_key_file_name,
+ "-outform", "PEM",
+ "-nosmimecap", "-nodetach",
+ "-nocerts", "-noattr"],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ output, err = process.communicate(text)
+ retcode = process.poll()
+ if retcode or "Error" in err:
+ LOG.error('Signing error: %s' % err)
+ raise subprocess.CalledProcessError(retcode, "openssl")
+ return output
+
+
+def cms_sign_token(text, signing_cert_file_name, signing_key_file_name):
+ output = cms_sign_text(text, signing_cert_file_name, signing_key_file_name)
+ return cms_to_token(output)
+
+
+def cms_to_token(cms_text):
+
+ start_delim = "-----BEGIN CMS-----"
+ end_delim = "-----END CMS-----"
+ signed_text = cms_text
+ signed_text = signed_text.replace('/', '-')
+ signed_text = signed_text.replace(start_delim, '')
+ signed_text = signed_text.replace(end_delim, '')
+ signed_text = signed_text.replace('\n', '')
+
+ return signed_text
+
+
+def cms_hash_token(token_id):
+ """
+ return: for ans1_token, returns the hash of the passed in token
+ otherwise, returns what it was passed in.
+ """
+ if token_id is None:
+ return None
+ if is_ans1_token(token_id):
+ hasher = hashlib.md5()
+ hasher.update(token_id)
+ return hasher.hexdigest()
+ else:
+ return token_id
diff --git a/keystoneclient/middleware/auth_token.py b/keystoneclient/middleware/auth_token.py
new file mode 100644
index 0000000..93af6e1
--- /dev/null
+++ b/keystoneclient/middleware/auth_token.py
@@ -0,0 +1,854 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010-2012 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+TOKEN-BASED AUTH MIDDLEWARE
+
+This WSGI component:
+
+* Verifies that incoming client requests have valid tokens by validating
+ tokens with the auth service.
+* Rejects unauthenticated requests UNLESS it is in 'delay_auth_decision'
+ mode, which means the final decision is delegated to the downstream WSGI
+ component (usually the OpenStack service)
+* Collects and forwards identity information based on a valid token
+ such as user name, tenant, etc
+
+Refer to: http://keystone.openstack.org/middlewarearchitecture.html
+
+HEADERS
+-------
+
+* Headers starting with HTTP\_ is a standard http header
+* Headers starting with HTTP_X is an extended http header
+
+Coming in from initial call from client or customer
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+HTTP_X_AUTH_TOKEN
+ The client token being passed in.
+
+HTTP_X_STORAGE_TOKEN
+ The client token being passed in (legacy Rackspace use) to support
+ swift/cloud files
+
+Used for communication between components
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+WWW-Authenticate
+ HTTP header returned to a user indicating which endpoint to use
+ to retrieve a new token
+
+What we add to the request for use by the OpenStack service
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+HTTP_X_IDENTITY_STATUS
+ 'Confirmed' or 'Invalid'
+ The underlying service will only see a value of 'Invalid' if the Middleware
+ is configured to run in 'delay_auth_decision' mode
+
+HTTP_X_TENANT_ID
+ Identity service managed unique identifier, string
+
+HTTP_X_TENANT_NAME
+ Unique tenant identifier, string
+
+HTTP_X_USER_ID
+ Identity-service managed unique identifier, string
+
+HTTP_X_USER_NAME
+ Unique user identifier, string
+
+HTTP_X_ROLES
+ Comma delimited list of case-sensitive Roles
+
+HTTP_X_SERVICE_CATALOG
+ json encoded keystone service catalog (optional).
+
+HTTP_X_TENANT
+ *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME
+ Keystone-assigned unique identifier, deprecated
+
+HTTP_X_USER
+ *Deprecated* in favor of HTTP_X_USER_ID and HTTP_X_USER_NAME
+ Unique user name, string
+
+HTTP_X_ROLE
+ *Deprecated* in favor of HTTP_X_ROLES
+ This is being renamed, and the new header contains the same data.
+
+"""
+
+import datetime
+import httplib
+import json
+import logging
+import os
+import stat
+import time
+import webob
+import webob.exc
+
+from keystoneclient.openstack.common import jsonutils
+from keystoneclient.common import cms
+from keystoneclient import utils
+from keystoneclient.openstack.common import timeutils
+
+CONF = None
+try:
+ from openstack.common import cfg
+ CONF = cfg.CONF
+except ImportError:
+ # cfg is not a library yet, try application copies
+ for app in 'nova', 'glance', 'quantum', 'cinder':
+ try:
+ cfg = __import__('%s.openstack.common.cfg' % app,
+ fromlist=['%s.openstack.common' % app])
+ # test which application middleware is running in
+ if hasattr(cfg, 'CONF') and 'config_file' in cfg.CONF:
+ CONF = cfg.CONF
+ break
+ except ImportError:
+ pass
+if not CONF:
+ from keystoneclient.openstack.common import cfg
+ CONF = cfg.CONF
+LOG = logging.getLogger(__name__)
+
+# alternative middleware configuration in the main application's
+# configuration file e.g. in nova.conf
+# [keystone_authtoken]
+# auth_host = 127.0.0.1
+# auth_port = 35357
+# auth_protocol = http
+# admin_tenant_name = admin
+# admin_user = admin
+# admin_password = badpassword
+opts = [
+ cfg.StrOpt('auth_admin_prefix', default=''),
+ cfg.StrOpt('auth_host', default='127.0.0.1'),
+ cfg.IntOpt('auth_port', default=35357),
+ cfg.StrOpt('auth_protocol', default='https'),
+ cfg.StrOpt('auth_uri', default=None),
+ cfg.BoolOpt('delay_auth_decision', default=False),
+ cfg.StrOpt('admin_token'),
+ cfg.StrOpt('admin_user'),
+ cfg.StrOpt('admin_password'),
+ cfg.StrOpt('admin_tenant_name', default='admin'),
+ cfg.StrOpt('certfile'),
+ cfg.StrOpt('keyfile'),
+ cfg.StrOpt('signing_dir'),
+ cfg.ListOpt('memcache_servers'),
+ cfg.IntOpt('token_cache_time', default=300),
+]
+CONF.register_opts(opts, group='keystone_authtoken')
+
+
+def will_expire_soon(expiry):
+ """ Determines if expiration is about to occur.
+
+ :param expiry: a datetime of the expected expiration
+ :returns: boolean : true if expiration is within 30 seconds
+ """
+ soon = (timeutils.utcnow() + datetime.timedelta(seconds=30))
+ return expiry < soon
+
+
+class InvalidUserToken(Exception):
+ pass
+
+
+class ServiceError(Exception):
+ pass
+
+
+class ConfigurationError(Exception):
+ pass
+
+
+class AuthProtocol(object):
+ """Auth Middleware that handles authenticating client calls."""
+
+ def __init__(self, app, conf):
+ LOG.info('Starting keystone auth_token middleware')
+ self.conf = conf
+ self.app = app
+
+ # delay_auth_decision means we still allow unauthenticated requests
+ # through and we let the downstream service make the final decision
+ self.delay_auth_decision = (self._conf_get('delay_auth_decision') in
+ (True, 'true', 't', '1', 'on', 'yes', 'y'))
+
+ # where to find the auth service (we use this to validate tokens)
+ self.auth_host = self._conf_get('auth_host')
+ self.auth_port = int(self._conf_get('auth_port'))
+ self.auth_protocol = self._conf_get('auth_protocol')
+ if self.auth_protocol == 'http':
+ self.http_client_class = httplib.HTTPConnection
+ else:
+ self.http_client_class = httplib.HTTPSConnection
+
+ self.auth_admin_prefix = self._conf_get('auth_admin_prefix')
+ self.auth_uri = self._conf_get('auth_uri')
+ if self.auth_uri is None:
+ self.auth_uri = '%s://%s:%s' % (self.auth_protocol,
+ self.auth_host,
+ self.auth_port)
+
+ # SSL
+ self.cert_file = self._conf_get('certfile')
+ self.key_file = self._conf_get('keyfile')
+
+ #signing
+ self.signing_dirname = self._conf_get('signing_dir')
+ if self.signing_dirname is None:
+ self.signing_dirname = '%s/keystone-signing' % os.environ['HOME']
+ LOG.info('Using %s as cache directory for signing certificate' %
+ self.signing_dirname)
+ if (os.path.exists(self.signing_dirname) and
+ not os.access(self.signing_dirname, os.W_OK)):
+ raise ConfigurationError("unable to access signing dir %s" %
+ self.signing_dirname)
+
+ if not os.path.exists(self.signing_dirname):
+ os.makedirs(self.signing_dirname)
+ #will throw IOError if it cannot change permissions
+ os.chmod(self.signing_dirname, stat.S_IRWXU)
+
+ val = '%s/signing_cert.pem' % self.signing_dirname
+ self.signing_cert_file_name = val
+ val = '%s/cacert.pem' % self.signing_dirname
+ self.ca_file_name = val
+ val = '%s/revoked.pem' % self.signing_dirname
+ self.revoked_file_name = val
+
+ # Credentials used to verify this component with the Auth service since
+ # validating tokens is a privileged call
+ self.admin_token = self._conf_get('admin_token')
+ self.admin_token_expiry = None
+ self.admin_user = self._conf_get('admin_user')
+ self.admin_password = self._conf_get('admin_password')
+ self.admin_tenant_name = self._conf_get('admin_tenant_name')
+
+ # Token caching via memcache
+ self._cache = None
+ self._iso8601 = None
+ memcache_servers = self._conf_get('memcache_servers')
+ # By default the token will be cached for 5 minutes
+ self.token_cache_time = int(self._conf_get('token_cache_time'))
+ self._token_revocation_list = None
+ self._token_revocation_list_fetched_time = None
+ cache_timeout = datetime.timedelta(seconds=0)
+ self.token_revocation_list_cache_timeout = cache_timeout
+ if memcache_servers:
+ try:
+ import memcache
+ import iso8601
+ LOG.info('Using memcache for caching token')
+ self._cache = memcache.Client(memcache_servers.split(','))
+ self._iso8601 = iso8601
+ except ImportError as e:
+ LOG.warn('disabled caching due to missing libraries %s', e)
+
+ def _conf_get(self, name):
+ # try config from paste-deploy first
+ if name in self.conf:
+ return self.conf[name]
+ else:
+ return CONF.keystone_authtoken[name]
+
+ def __call__(self, env, start_response):
+ """Handle incoming request.
+
+ Authenticate send downstream on success. Reject request if
+ we can't authenticate.
+
+ """
+ LOG.debug('Authenticating user token')
+ try:
+ self._remove_auth_headers(env)
+ user_token = self._get_user_token_from_header(env)
+ token_info = self._validate_user_token(user_token)
+ user_headers = self._build_user_headers(token_info)
+ self._add_headers(env, user_headers)
+ return self.app(env, start_response)
+
+ except InvalidUserToken:
+ if self.delay_auth_decision:
+ LOG.info('Invalid user token - deferring reject downstream')
+ self._add_headers(env, {'X-Identity-Status': 'Invalid'})
+ return self.app(env, start_response)
+ else:
+ LOG.info('Invalid user token - rejecting request')
+ return self._reject_request(env, start_response)
+
+ except ServiceError as e:
+ LOG.critical('Unable to obtain admin token: %s' % e)
+ resp = webob.exc.HTTPServiceUnavailable()
+ return resp(env, start_response)
+
+ def _remove_auth_headers(self, env):
+ """Remove headers so a user can't fake authentication.
+
+ :param env: wsgi request environment
+
+ """
+ auth_headers = (
+ 'X-Identity-Status',
+ 'X-Tenant-Id',
+ 'X-Tenant-Name',
+ 'X-User-Id',
+ 'X-User-Name',
+ 'X-Roles',
+ 'X-Service-Catalog',
+ # Deprecated
+ 'X-User',
+ 'X-Tenant',
+ 'X-Role',
+ )
+ LOG.debug('Removing headers from request environment: %s' %
+ ','.join(auth_headers))
+ self._remove_headers(env, auth_headers)
+
+ def _get_user_token_from_header(self, env):
+ """Get token id from request.
+
+ :param env: wsgi request environment
+ :return token id
+ :raises InvalidUserToken if no token is provided in request
+
+ """
+ token = self._get_header(env, 'X-Auth-Token',
+ self._get_header(env, 'X-Storage-Token'))
+ if token:
+ return token
+ else:
+ LOG.warn("Unable to find authentication token in headers: %s", env)
+ raise InvalidUserToken('Unable to find token in headers')
+
+ def _reject_request(self, env, start_response):
+ """Redirect client to auth server.
+
+ :param env: wsgi request environment
+ :param start_response: wsgi response callback
+ :returns HTTPUnauthorized http response
+
+ """
+ headers = [('WWW-Authenticate', 'Keystone uri=\'%s\'' % self.auth_uri)]
+ resp = webob.exc.HTTPUnauthorized('Authentication required', headers)
+ return resp(env, start_response)
+
+ def get_admin_token(self):
+ """Return admin token, possibly fetching a new one.
+
+ if self.admin_token_expiry is set from fetching an admin token, check
+ it for expiration, and request a new token is the existing token
+ is about to expire.
+
+ :return admin token id
+ :raise ServiceError when unable to retrieve token from keystone
+
+ """
+ if self.admin_token_expiry:
+ if will_expire_soon(self.admin_token_expiry):
+ self.admin_token = None
+
+ if not self.admin_token:
+ (self.admin_token,
+ self.admin_token_expiry) = self._request_admin_token()
+
+ return self.admin_token
+
+ def _get_http_connection(self):
+ if self.auth_protocol == 'http':
+ return self.http_client_class(self.auth_host, self.auth_port)
+ else:
+ return self.http_client_class(self.auth_host,
+ self.auth_port,
+ self.key_file,
+ self.cert_file)
+
+ def _http_request(self, method, path):
+ """HTTP request helper used to make unspecified content type requests.
+
+ :param method: http method
+ :param path: relative request url
+ :return (http response object)
+ :raise ServerError when unable to communicate with keystone
+
+ """
+ conn = self._get_http_connection()
+
+ try:
+ conn.request(method, path)
+ response = conn.getresponse()
+ body = response.read()
+ except Exception as e:
+ LOG.error('HTTP connection exception: %s' % e)
+ raise ServiceError('Unable to communicate with keystone')
+ finally:
+ conn.close()
+
+ return response, body
+
+ def _json_request(self, method, path, body=None, additional_headers=None):
+ """HTTP request helper used to make json requests.
+
+ :param method: http method
+ :param path: relative request url
+ :param body: dict to encode to json as request body. Optional.
+ :param additional_headers: dict of additional headers to send with
+ http request. Optional.
+ :return (http response object, response body parsed as json)
+ :raise ServerError when unable to communicate with keystone
+
+ """
+ conn = self._get_http_connection()
+
+ kwargs = {
+ 'headers': {
+ 'Content-type': 'application/json',
+ 'Accept': 'application/json',
+ },
+ }
+
+ if additional_headers:
+ kwargs['headers'].update(additional_headers)
+
+ if body:
+ kwargs['body'] = jsonutils.dumps(body)
+
+ full_path = self.auth_admin_prefix + path
+ try:
+ conn.request(method, full_path, **kwargs)
+ response = conn.getresponse()
+ body = response.read()
+ except Exception as e:
+ LOG.error('HTTP connection exception: %s' % e)
+ raise ServiceError('Unable to communicate with keystone')
+ finally:
+ conn.close()
+
+ try:
+ data = jsonutils.loads(body)
+ except ValueError:
+ LOG.debug('Keystone did not return json-encoded body')
+ data = {}
+
+ return response, data
+
+ def _request_admin_token(self):
+ """Retrieve new token as admin user from keystone.
+
+ :return token id upon success
+ :raises ServerError when unable to communicate with keystone
+
+ """
+ params = {
+ 'auth': {
+ 'passwordCredentials': {
+ 'username': self.admin_user,
+ 'password': self.admin_password,
+ },
+ 'tenantName': self.admin_tenant_name,
+ }
+ }
+
+ response, data = self._json_request('POST',
+ '/v2.0/tokens',
+ body=params)
+
+ try:
+ token = data['access']['token']['id']
+ expiry = data['access']['token']['expires']
+ assert token
+ assert expiry
+ datetime_expiry = timeutils.parse_isotime(expiry)
+ return (token, timeutils.normalize_time(datetime_expiry))
+ except (AssertionError, KeyError):
+ LOG.warn("Unexpected response from keystone service: %s", data)
+ raise ServiceError('invalid json response')
+ except (ValueError):
+ LOG.warn("Unable to parse expiration time from token: %s", data)
+ raise ServiceError('invalid json response')
+
+ def _validate_user_token(self, user_token, retry=True):
+ """Authenticate user using PKI
+
+ :param user_token: user's token id
+ :param retry: Ignored, as it is not longer relevant
+ :return uncrypted body of the token if the token is valid
+ :raise InvalidUserToken if token is rejected
+ :no longer raises ServiceError since it no longer makes RPC
+
+ """
+ try:
+ token_id = cms.cms_hash_token(user_token)
+ cached = self._cache_get(token_id)
+ if cached:
+ return cached
+ if cms.is_ans1_token(user_token):
+ verified = self.verify_signed_token(user_token)
+ data = json.loads(verified)
+ else:
+ data = self.verify_uuid_token(user_token, retry)
+ self._cache_put(token_id, data)
+ return data
+ except Exception as e:
+ LOG.debug('Token validation failure.', exc_info=True)
+ self._cache_store_invalid(user_token)
+ LOG.warn("Authorization failed for token %s", user_token)
+ raise InvalidUserToken('Token authorization failed')
+
+ def _build_user_headers(self, token_info):
+ """Convert token object into headers.
+
+ Build headers that represent authenticated user:
+ * X_IDENTITY_STATUS: Confirmed or Invalid
+ * X_TENANT_ID: id of tenant if tenant is present
+ * X_TENANT_NAME: name of tenant if tenant is present
+ * X_USER_ID: id of user
+ * X_USER_NAME: name of user
+ * X_ROLES: list of roles
+ * X_SERVICE_CATALOG: service catalog
+
+ Additional (deprecated) headers include:
+ * X_USER: name of user
+ * X_TENANT: For legacy compatibility before we had ID and Name
+ * X_ROLE: list of roles
+
+ :param token_info: token object returned by keystone on authentication
+ :raise InvalidUserToken when unable to parse token object
+
+ """
+ user = token_info['access']['user']
+ token = token_info['access']['token']
+ roles = ','.join([role['name'] for role in user.get('roles', [])])
+
+ def get_tenant_info():
+ """Returns a (tenant_id, tenant_name) tuple from context."""
+ def essex():
+ """Essex puts the tenant ID and name on the token."""
+ return (token['tenant']['id'], token['tenant']['name'])
+
+ def pre_diablo():
+ """Pre-diablo, Keystone only provided tenantId."""
+ return (token['tenantId'], token['tenantId'])
+
+ def default_tenant():
+ """Assume the user's default tenant."""
+ return (user['tenantId'], user['tenantName'])
+
+ for method in [essex, pre_diablo, default_tenant]:
+ try:
+ return method()
+ except KeyError:
+ pass
+
+ raise InvalidUserToken('Unable to determine tenancy.')
+
+ tenant_id, tenant_name = get_tenant_info()
+
+ user_id = user['id']
+ user_name = user['name']
+
+ rval = {
+ 'X-Identity-Status': 'Confirmed',
+ 'X-Tenant-Id': tenant_id,
+ 'X-Tenant-Name': tenant_name,
+ 'X-User-Id': user_id,
+ 'X-User-Name': user_name,
+ 'X-Roles': roles,
+ # Deprecated
+ 'X-User': user_name,
+ 'X-Tenant': tenant_name,
+ 'X-Role': roles,
+ }
+
+ try:
+ catalog = token_info['access']['serviceCatalog']
+ rval['X-Service-Catalog'] = jsonutils.dumps(catalog)
+ except KeyError:
+ pass
+
+ return rval
+
+ def _header_to_env_var(self, key):
+ """Convert header to wsgi env variable.
+
+ :param key: http header name (ex. 'X-Auth-Token')
+ :return wsgi env variable name (ex. 'HTTP_X_AUTH_TOKEN')
+
+ """
+ return 'HTTP_%s' % key.replace('-', '_').upper()
+
+ def _add_headers(self, env, headers):
+ """Add http headers to environment."""
+ for (k, v) in headers.iteritems():
+ env_key = self._header_to_env_var(k)
+ env[env_key] = v
+
+ def _remove_headers(self, env, keys):
+ """Remove http headers from environment."""
+ for k in keys:
+ env_key = self._header_to_env_var(k)
+ try:
+ del env[env_key]
+ except KeyError:
+ pass
+
+ def _get_header(self, env, key, default=None):
+ """Get http header from environment."""
+ env_key = self._header_to_env_var(key)
+ return env.get(env_key, default)
+
+ def _cache_get(self, token):
+ """Return token information from cache.
+
+ If token is invalid raise InvalidUserToken
+ return token only if fresh (not expired).
+ """
+ if self._cache and token:
+ key = 'tokens/%s' % token
+ cached = self._cache.get(key)
+ if cached == 'invalid':
+ LOG.debug('Cached Token %s is marked unauthorized', token)
+ raise InvalidUserToken('Token authorization failed')
+ if cached:
+ data, expires = cached
+ if time.time() < float(expires):
+ LOG.debug('Returning cached token %s', token)
+ return data
+ else:
+ LOG.debug('Cached Token %s seems expired', token)
+
+ def _cache_put(self, token, data):
+ """Put token data into the cache.
+
+ Stores the parsed expire date in cache allowing
+ quick check of token freshness on retrieval.
+ """
+ if self._cache and data:
+ key = 'tokens/%s' % token
+ if 'token' in data.get('access', {}):
+ timestamp = data['access']['token']['expires']
+ expires = self._iso8601.parse_date(timestamp).strftime('%s')
+ else:
+ LOG.error('invalid token format')
+ return
+ LOG.debug('Storing %s token in memcache', token)
+ self._cache.set(key,
+ (data, expires),
+ time=self.token_cache_time)
+
+ def _cache_store_invalid(self, token):
+ """Store invalid token in cache."""
+ if self._cache:
+ key = 'tokens/%s' % token
+ LOG.debug('Marking token %s as unauthorized in memcache', token)
+ self._cache.set(key,
+ 'invalid',
+ time=self.token_cache_time)
+
+ def cert_file_missing(self, called_proc_err, file_name):
+ return (called_proc_err.output.find(file_name)
+ and not os.path.exists(file_name))
+
+ def verify_uuid_token(self, user_token, retry=True):
+ """Authenticate user token with keystone.
+
+ :param user_token: user's token id
+ :param retry: flag that forces the middleware to retry
+ user authentication when an indeterminate
+ response is received. Optional.
+ :return token object received from keystone on success
+ :raise InvalidUserToken if token is rejected
+ :raise ServiceError if unable to authenticate token
+
+ """
+
+ headers = {'X-Auth-Token': self.get_admin_token()}
+ response, data = self._json_request('GET',
+ '/v2.0/tokens/%s' % user_token,
+ additional_headers=headers)
+
+ if response.status == 200:
+ self._cache_put(user_token, data)
+ return data
+ if response.status == 404:
+ # FIXME(ja): I'm assuming the 404 status means that user_token is
+ # invalid - not that the admin_token is invalid
+ self._cache_store_invalid(user_token)
+ LOG.warn("Authorization failed for token %s", user_token)
+ raise InvalidUserToken('Token authorization failed')
+ if response.status == 401:
+ LOG.info('Keystone rejected admin token %s, resetting', headers)
+ self.admin_token = None
+ else:
+ LOG.error('Bad response code while validating token: %s' %
+ response.status)
+ if retry:
+ LOG.info('Retrying validation')
+ return self._validate_user_token(user_token, False)
+ else:
+ LOG.warn("Invalid user token: %s. Keystone response: %s.",
+ user_token, data)
+
+ raise InvalidUserToken()
+
+ def is_signed_token_revoked(self, signed_text):
+ """Indicate whether the token appears in the revocation list."""
+ revocation_list = self.token_revocation_list
+ revoked_tokens = revocation_list.get('revoked', [])
+ if not revoked_tokens:
+ return
+ revoked_ids = (x['id'] for x in revoked_tokens)
+ token_id = utils.hash_signed_token(signed_text)
+ for revoked_id in revoked_ids:
+ if token_id == revoked_id:
+ LOG.debug('Token %s is marked as having been revoked',
+ token_id)
+ return True
+ return False
+
+ def cms_verify(self, data):
+ """Verifies the signature of the provided data's IAW CMS syntax.
+
+ If either of the certificate files are missing, fetch them and
+ retry.
+ """
+ while True:
+ try:
+ output = cms.cms_verify(data, self.signing_cert_file_name,
+ self.ca_file_name)
+ except cms.subprocess.CalledProcessError as err:
+ if self.cert_file_missing(err, self.signing_cert_file_name):
+ self.fetch_signing_cert()
+ continue
+ if self.cert_file_missing(err, self.ca_file_name):
+ self.fetch_ca_cert()
+ continue
+ raise err
+ return output
+
+ def verify_signed_token(self, signed_text):
+ """Check that the token is unrevoked and has a valid signature."""
+ if self.is_signed_token_revoked(signed_text):
+ raise InvalidUserToken('Token has been revoked')
+
+ formatted = cms.token_to_cms(signed_text)
+ return self.cms_verify(formatted)
+
+ @property
+ def token_revocation_list_fetched_time(self):
+ if not self._token_revocation_list_fetched_time:
+ # If the fetched list has been written to disk, use its
+ # modification time.
+ if os.path.exists(self.revoked_file_name):
+ mtime = os.path.getmtime(self.revoked_file_name)
+ fetched_time = datetime.datetime.fromtimestamp(mtime)
+ # Otherwise the list will need to be fetched.
+ else:
+ fetched_time = datetime.datetime.min
+ self._token_revocation_list_fetched_time = fetched_time
+ return self._token_revocation_list_fetched_time
+
+ @token_revocation_list_fetched_time.setter
+ def token_revocation_list_fetched_time(self, value):
+ self._token_revocation_list_fetched_time = value
+
+ @property
+ def token_revocation_list(self):
+ timeout = (self.token_revocation_list_fetched_time +
+ self.token_revocation_list_cache_timeout)
+ list_is_current = timeutils.utcnow() < timeout
+ if list_is_current:
+ # Load the list from disk if required
+ if not self._token_revocation_list:
+ with open(self.revoked_file_name, 'r') as f:
+ self._token_revocation_list = jsonutils.loads(f.read())
+ else:
+ self.token_revocation_list = self.fetch_revocation_list()
+ return self._token_revocation_list
+
+ @token_revocation_list.setter
+ def token_revocation_list(self, value):
+ """Save a revocation list to memory and to disk.
+
+ :param value: A json-encoded revocation list
+
+ """
+ self._token_revocation_list = jsonutils.loads(value)
+ self.token_revocation_list_fetched_time = timeutils.utcnow()
+ with open(self.revoked_file_name, 'w') as f:
+ f.write(value)
+
+ def fetch_revocation_list(self, retry=True):
+ headers = {'X-Auth-Token': self.get_admin_token()}
+ response, data = self._json_request('GET', '/v2.0/tokens/revoked',
+ additional_headers=headers)
+ if response.status == 401:
+ if retry:
+ LOG.info('Keystone rejected admin token %s, resetting admin '
+ 'token', headers)
+ self.admin_token = None
+ return self.fetch_revocation_list(retry=False)
+ if response.status != 200:
+ raise ServiceError('Unable to fetch token revocation list.')
+ if (not 'signed' in data):
+ raise ServiceError('Revocation list inmproperly formatted.')
+ return self.cms_verify(data['signed'])
+
+ def fetch_signing_cert(self):
+ response, data = self._http_request('GET',
+ '/v2.0/certificates/signing')
+ try:
+ #todo check response
+ certfile = open(self.signing_cert_file_name, 'w')
+ certfile.write(data)
+ certfile.close()
+ except (AssertionError, KeyError):
+ LOG.warn("Unexpected response from keystone service: %s", data)
+ raise ServiceError('invalid json response')
+
+ def fetch_ca_cert(self):
+ response, data = self._http_request('GET',
+ '/v2.0/certificates/ca')
+ try:
+ #todo check response
+ certfile = open(self.ca_file_name, 'w')
+ certfile.write(data)
+ certfile.close()
+ except (AssertionError, KeyError):
+ LOG.warn("Unexpected response from keystone service: %s", data)
+ raise ServiceError('invalid json response')
+
+
+def filter_factory(global_conf, **local_conf):
+ """Returns a WSGI filter app for use with paste.deploy."""
+ conf = global_conf.copy()
+ conf.update(local_conf)
+
+ def auth_filter(app):
+ return AuthProtocol(app, conf)
+ return auth_filter
+
+
+def app_factory(global_conf, **local_conf):
+ conf = global_conf.copy()
+ conf.update(local_conf)
+ return AuthProtocol(None, conf)
diff --git a/keystoneclient/middleware/test.py b/keystoneclient/middleware/test.py
new file mode 100644
index 0000000..e5c1171
--- /dev/null
+++ b/keystoneclient/middleware/test.py
@@ -0,0 +1,67 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+#
+# Test support for middleware authentication
+#
+
+import os
+import sys
+
+
+ROOTDIR = os.path.dirname(os.path.abspath(os.curdir))
+
+
+def rootdir(*p):
+ return os.path.join(ROOTDIR, *p)
+
+
+class NoModule(object):
+ """A mixin class to provide support for unloading/disabling modules."""
+
+ def __init__(self, *args, **kw):
+ super(NoModule, self).__init__(*args, **kw)
+ self._finders = []
+ self._cleared_modules = {}
+
+ def tearDown(self):
+ super(NoModule, self).tearDown()
+ for finder in self._finders:
+ sys.meta_path.remove(finder)
+ sys.modules.update(self._cleared_modules)
+
+ def clear_module(self, module):
+ cleared_modules = {}
+ for fullname in sys.modules.keys():
+ if fullname == module or fullname.startswith(module + '.'):
+ cleared_modules[fullname] = sys.modules.pop(fullname)
+ return cleared_modules
+
+ def disable_module(self, module):
+ """Ensure ImportError for the specified module."""
+
+ # Clear 'module' references in sys.modules
+ self._cleared_modules.update(self.clear_module(module))
+
+ # Disallow further imports of 'module'
+ class NoModule(object):
+ def find_module(self, fullname, path):
+ if fullname == module or fullname.startswith(module + '.'):
+ raise ImportError
+
+ finder = NoModule()
+ self._finders.append(finder)
+ sys.meta_path.insert(0, finder)
diff --git a/keystoneclient/utils.py b/keystoneclient/utils.py
index 36ec41b..225afe7 100644
--- a/keystoneclient/utils.py
+++ b/keystoneclient/utils.py
@@ -1,4 +1,5 @@
import uuid
+import hashlib
import prettytable
@@ -114,3 +115,9 @@ def string_to_bool(arg):
return arg
return arg.strip().lower() in ('t', 'true', 'yes', '1')
+
+
+def hash_signed_token(signed_text):
+ hash_ = hashlib.md5()
+ hash_.update(signed_text)
+ return hash_.hexdigest()
diff --git a/tests/test_auth_token_middleware.py b/tests/test_auth_token_middleware.py
new file mode 100644
index 0000000..0c15504
--- /dev/null
+++ b/tests/test_auth_token_middleware.py
@@ -0,0 +1,667 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import datetime
+import iso8601
+import os
+import string
+import tempfile
+import unittest2 as unittest
+
+import webob
+
+from keystoneclient.common import cms
+from keystoneclient import utils
+from keystoneclient.middleware import auth_token
+from keystoneclient.openstack.common import jsonutils
+from keystoneclient.openstack.common import timeutils
+from keystoneclient.middleware import test
+
+
+CERTDIR = test.rootdir("python-keystoneclient/examples/pki/certs")
+KEYDIR = test.rootdir("python-keystoneclient/examples/pki/private")
+CMSDIR = test.rootdir("python-keystoneclient/examples/pki/cms")
+SIGNING_CERT = os.path.join(CERTDIR, 'signing_cert.pem')
+SIGNING_KEY = os.path.join(KEYDIR, 'signing_key.pem')
+CA = os.path.join(CERTDIR, 'ca.pem')
+
+REVOCATION_LIST = None
+REVOKED_TOKEN = None
+REVOKED_TOKEN_HASH = None
+SIGNED_REVOCATION_LIST = None
+SIGNED_TOKEN_SCOPED = None
+SIGNED_TOKEN_UNSCOPED = None
+SIGNED_TOKEN_SCOPED_KEY = None
+SIGNED_TOKEN_UNSCOPED_KEY = None
+
+VALID_SIGNED_REVOCATION_LIST = None
+
+UUID_TOKEN_DEFAULT = "ec6c0710ec2f471498484c1b53ab4f9d"
+UUID_TOKEN_NO_SERVICE_CATALOG = '8286720fbe4941e69fa8241723bb02df'
+UUID_TOKEN_UNSCOPED = '731f903721c14827be7b2dc912af7776'
+VALID_DIABLO_TOKEN = 'b0cf19b55dbb4f20a6ee18e6c6cf1726'
+
+INVALID_SIGNED_TOKEN = string.replace(
+ """AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
+CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
+DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
+EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
+FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
+0000000000000000000000000000000000000000000000000000000000000000
+1111111111111111111111111111111111111111111111111111111111111111
+2222222222222222222222222222222222222222222222222222222222222222
+3333333333333333333333333333333333333333333333333333333333333333
+4444444444444444444444444444444444444444444444444444444444444444
+5555555555555555555555555555555555555555555555555555555555555555
+6666666666666666666666666666666666666666666666666666666666666666
+7777777777777777777777777777777777777777777777777777777777777777
+8888888888888888888888888888888888888888888888888888888888888888
+9999999999999999999999999999999999999999999999999999999999999999
+0000000000000000000000000000000000000000000000000000000000000000
+xg==""", "\n", "")
+
+# JSON responses keyed by token ID
+TOKEN_RESPONSES = {
+ UUID_TOKEN_DEFAULT: {
+ 'access': {
+ 'token': {
+ 'id': UUID_TOKEN_DEFAULT,
+ 'expires': '2999-01-01T00:00:10Z',
+ 'tenant': {
+ 'id': 'tenant_id1',
+ 'name': 'tenant_name1',
+ },
+ },
+ 'user': {
+ 'id': 'user_id1',
+ 'name': 'user_name1',
+ 'roles': [
+ {'name': 'role1'},
+ {'name': 'role2'},
+ ],
+ },
+ 'serviceCatalog': {}
+ },
+ },
+ VALID_DIABLO_TOKEN: {
+ 'access': {
+ 'token': {
+ 'id': VALID_DIABLO_TOKEN,
+ 'expires': '2999-01-01T00:00:10',
+ 'tenantId': 'tenant_id1',
+ },
+ 'user': {
+ 'id': 'user_id1',
+ 'name': 'user_name1',
+ 'roles': [
+ {'name': 'role1'},
+ {'name': 'role2'},
+ ],
+ },
+ },
+ },
+ UUID_TOKEN_UNSCOPED: {
+ 'access': {
+ 'token': {
+ 'id': UUID_TOKEN_UNSCOPED,
+ 'expires': '2999-01-01T00:00:10Z',
+ },
+ 'user': {
+ 'id': 'user_id1',
+ 'name': 'user_name1',
+ 'roles': [
+ {'name': 'role1'},
+ {'name': 'role2'},
+ ],
+ },
+ },
+ },
+ UUID_TOKEN_NO_SERVICE_CATALOG: {
+ 'access': {
+ 'token': {
+ 'id': 'valid-token',
+ 'expires': '2999-01-01T00:00:10Z',
+ 'tenant': {
+ 'id': 'tenant_id1',
+ 'name': 'tenant_name1',
+ },
+ },
+ 'user': {
+ 'id': 'user_id1',
+ 'name': 'user_name1',
+ 'roles': [
+ {'name': 'role1'},
+ {'name': 'role2'},
+ ],
+ }
+ },
+ },
+}
+
+FAKE_RESPONSE_STACK = []
+
+
+# The data for these tests are signed using openssl and are stored in files
+# in the signing subdirectory. In order to keep the values consistent between
+# the tests and the signed documents, we read them in for use in the tests.
+def setUpModule(self):
+ signing_path = CMSDIR
+ with open(os.path.join(signing_path, 'auth_token_scoped.pem')) as f:
+ self.SIGNED_TOKEN_SCOPED = cms.cms_to_token(f.read())
+ with open(os.path.join(signing_path, 'auth_token_unscoped.pem')) as f:
+ self.SIGNED_TOKEN_UNSCOPED = cms.cms_to_token(f.read())
+ with open(os.path.join(signing_path, 'auth_token_revoked.pem')) as f:
+ self.REVOKED_TOKEN = cms.cms_to_token(f.read())
+ self.REVOKED_TOKEN_HASH = utils.hash_signed_token(self.REVOKED_TOKEN)
+ with open(os.path.join(signing_path, 'revocation_list.json')) as f:
+ self.REVOCATION_LIST = jsonutils.loads(f.read())
+ with open(os.path.join(signing_path, 'revocation_list.pem')) as f:
+ self.VALID_SIGNED_REVOCATION_LIST = jsonutils.dumps(
+ {'signed': f.read()})
+ self.SIGNED_TOKEN_SCOPED_KEY =\
+ cms.cms_hash_token(self.SIGNED_TOKEN_SCOPED)
+ self.SIGNED_TOKEN_UNSCOPED_KEY =\
+ cms.cms_hash_token(self.SIGNED_TOKEN_UNSCOPED)
+
+ self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED_KEY] = {
+ 'access': {
+ 'token': {
+ 'id': self.SIGNED_TOKEN_SCOPED_KEY,
+ },
+ 'user': {
+ 'id': 'user_id1',
+ 'name': 'user_name1',
+ 'tenantId': 'tenant_id1',
+ 'tenantName': 'tenant_name1',
+ 'roles': [
+ {'name': 'role1'},
+ {'name': 'role2'},
+ ],
+ },
+ },
+ }
+
+ self.TOKEN_RESPONSES[SIGNED_TOKEN_UNSCOPED_KEY] = {
+ 'access': {
+ 'token': {
+ 'id': SIGNED_TOKEN_UNSCOPED_KEY,
+ },
+ 'user': {
+ 'id': 'user_id1',
+ 'name': 'user_name1',
+ 'roles': [
+ {'name': 'role1'},
+ {'name': 'role2'},
+ ],
+ },
+ },
+ },
+
+
+class FakeMemcache(object):
+ def __init__(self):
+ self.set_key = None
+ self.set_value = None
+ self.token_expiration = None
+
+ def get(self, key):
+ data = TOKEN_RESPONSES[SIGNED_TOKEN_SCOPED_KEY].copy()
+ if not data or key != "tokens/%s" % (data['access']['token']['id']):
+ return
+ if not self.token_expiration:
+ dt = datetime.datetime.now() + datetime.timedelta(minutes=5)
+ self.token_expiration = dt.strftime("%s")
+ dt = datetime.datetime.now() + datetime.timedelta(hours=24)
+ ks_expires = dt.isoformat()
+ data['access']['token']['expires'] = ks_expires
+ return (data, str(self.token_expiration))
+
+ def set(self, key, value, time=None):
+ self.set_value = value
+ self.set_key = key
+
+
+class FakeHTTPResponse(object):
+ def __init__(self, status, body):
+ self.status = status
+ self.body = body
+
+ def read(self):
+ return self.body
+
+
+class FakeStackHTTPConnection(object):
+
+ def __init__(self, *args, **kwargs):
+ pass
+
+ def getresponse(self):
+ if len(FAKE_RESPONSE_STACK):
+ return FAKE_RESPONSE_STACK.pop()
+ return FakeHTTPResponse(500, jsonutils.dumps('UNEXPECTED RESPONSE'))
+
+ def request(self, *_args, **_kwargs):
+ pass
+
+ def close(self):
+ pass
+
+
+class FakeHTTPConnection(object):
+
+ last_requested_url = ''
+
+ def __init__(self, *args):
+ self.send_valid_revocation_list = True
+
+ def request(self, method, path, **kwargs):
+ """Fakes out several http responses.
+
+ If a POST request is made, we assume the calling code is trying
+ to get a new admin token.
+
+ If a GET request is made to validate a token, return success
+ if the token is 'token1'. If a different token is provided, return
+ a 404, indicating an unknown (therefore unauthorized) token.
+
+ """
+ FakeHTTPConnection.last_requested_url = path
+ if method == 'POST':
+ status = 200
+ body = jsonutils.dumps({
+ 'access': {
+ 'token': {'id': 'admin_token2'},
+ },
+ })
+
+ else:
+ token_id = path.rsplit('/', 1)[1]
+ if token_id in TOKEN_RESPONSES.keys():
+ status = 200
+ body = jsonutils.dumps(TOKEN_RESPONSES[token_id])
+ elif token_id == "revoked":
+ status = 200
+ body = SIGNED_REVOCATION_LIST
+ else:
+ status = 404
+ body = str()
+
+ self.resp = FakeHTTPResponse(status, body)
+
+ def getresponse(self):
+ return self.resp
+
+ def close(self):
+ pass
+
+
+class FakeApp(object):
+ """This represents a WSGI app protected by the auth_token middleware."""
+ def __init__(self, expected_env=None):
+ expected_env = expected_env or {}
+ self.expected_env = {
+ 'HTTP_X_IDENTITY_STATUS': 'Confirmed',
+ 'HTTP_X_TENANT_ID': 'tenant_id1',
+ 'HTTP_X_TENANT_NAME': 'tenant_name1',
+ 'HTTP_X_USER_ID': 'user_id1',
+ 'HTTP_X_USER_NAME': 'user_name1',
+ 'HTTP_X_ROLES': 'role1,role2',
+ 'HTTP_X_USER': 'user_name1', # deprecated (diablo-compat)
+ 'HTTP_X_TENANT': 'tenant_name1', # deprecated (diablo-compat)
+ 'HTTP_X_ROLE': 'role1,role2', # deprecated (diablo-compat)
+ }
+ self.expected_env.update(expected_env)
+
+ def __call__(self, env, start_response):
+ for k, v in self.expected_env.items():
+ assert env[k] == v, '%s != %s' % (env[k], v)
+
+ resp = webob.Response()
+ resp.body = 'SUCCESS'
+ return resp(env, start_response)
+
+
+class BaseAuthTokenMiddlewareTest(unittest.TestCase):
+
+ def setUp(self, expected_env=None):
+ expected_env = expected_env or {}
+
+ conf = {
+ 'admin_token': 'admin_token1',
+ 'auth_host': 'keystone.example.com',
+ 'auth_port': 1234,
+ 'auth_admin_prefix': '/testadmin',
+ 'signing_dir': CERTDIR,
+ }
+
+ self.middleware = auth_token.AuthProtocol(FakeApp(expected_env), conf)
+ self.middleware.http_client_class = FakeHTTPConnection
+ self.middleware._iso8601 = iso8601
+
+ self.response_status = None
+ self.response_headers = None
+ self.middleware.revoked_file_name = tempfile.mkstemp()[1]
+ cache_timeout = datetime.timedelta(days=1)
+ self.middleware.token_revocation_list_cache_timeout = cache_timeout
+ self.middleware.token_revocation_list = jsonutils.dumps(
+ {"revoked": [], "extra": "success"})
+
+ signed_list = 'SIGNED_REVOCATION_LIST'
+ valid_signed_list = 'VALID_SIGNED_REVOCATION_LIST'
+ globals()[signed_list] = globals()[valid_signed_list]
+
+ super(BaseAuthTokenMiddlewareTest, self).setUp()
+
+ def tearDown(self):
+ super(BaseAuthTokenMiddlewareTest, self).tearDown()
+ try:
+ os.remove(self.middleware.revoked_file_name)
+ except OSError:
+ pass
+
+ def start_fake_response(self, status, headers):
+ self.response_status = int(status.split(' ', 1)[0])
+ self.response_headers = dict(headers)
+
+
+class StackResponseAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest):
+ """Auth Token middleware test setup that allows the tests to define
+ a stack of responses to HTTP requests in the test and get those
+ responses back in sequence for testing.
+
+ Example::
+
+ resp1 = FakeHTTPResponse(401, jsonutils.dumps(''))
+ resp2 = FakeHTTPResponse(200, jsonutils.dumps({
+ 'access': {
+ 'token': {'id': 'admin_token2'},
+ },
+ })
+ FAKE_RESPONSE_STACK.append(resp1)
+ FAKE_RESPONSE_STACK.append(resp2)
+
+ ... do your testing code here ...
+
+ """
+
+ def setUp(self, expected_env=None):
+ super(StackResponseAuthTokenMiddlewareTest, self).setUp(expected_env)
+ self.middleware.http_client_class = FakeStackHTTPConnection
+
+ def test_fetch_revocation_list_with_expire(self):
+ # first response to revocation list should return 401 Unauthorized
+ # to pretend to be an expired token
+ resp1 = FakeHTTPResponse(200, jsonutils.dumps({
+ 'access': {
+ 'token': {'id': 'admin_token2'},
+ },
+ }))
+ resp2 = FakeHTTPResponse(401, jsonutils.dumps(''))
+ resp3 = FakeHTTPResponse(200, jsonutils.dumps({
+ 'access': {
+ 'token': {'id': 'admin_token2'},
+ },
+ }))
+ resp4 = FakeHTTPResponse(200, SIGNED_REVOCATION_LIST)
+
+ # first get_admin_token() call
+ FAKE_RESPONSE_STACK.append(resp1)
+ # request revocation list, get "unauthorized" due to simulated expired
+ # token
+ FAKE_RESPONSE_STACK.append(resp2)
+ # request a new admin_token
+ FAKE_RESPONSE_STACK.append(resp3)
+ # request revocation list, get the revocation list properly
+ FAKE_RESPONSE_STACK.append(resp4)
+
+ fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list())
+ self.assertEqual(fetched_list, REVOCATION_LIST)
+
+
+class DiabloAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest):
+ """Auth Token middleware should understand Diablo keystone responses."""
+ def setUp(self):
+ # pre-diablo only had Tenant ID, which was also the Name
+ expected_env = {
+ 'HTTP_X_TENANT_ID': 'tenant_id1',
+ 'HTTP_X_TENANT_NAME': 'tenant_id1',
+ # now deprecated (diablo-compat)
+ 'HTTP_X_TENANT': 'tenant_id1',
+ }
+ super(DiabloAuthTokenMiddlewareTest, self).setUp(expected_env)
+
+ def test_valid_diablo_response(self):
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = VALID_DIABLO_TOKEN
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.response_status, 200)
+
+
+class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest):
+ def assert_valid_request_200(self, token):
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = token
+ body = self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.response_status, 200)
+ self.assertTrue(req.headers.get('X-Service-Catalog'))
+ self.assertEqual(body, ['SUCCESS'])
+
+ def test_valid_uuid_request(self):
+ self.assert_valid_request_200(UUID_TOKEN_DEFAULT)
+ self.assertEqual("/testadmin/v2.0/tokens/%s" % UUID_TOKEN_DEFAULT,
+ FakeHTTPConnection.last_requested_url)
+
+ def test_valid_signed_request(self):
+ FakeHTTPConnection.last_requested_url = ''
+ self.assert_valid_request_200(SIGNED_TOKEN_SCOPED)
+ self.assertEqual(self.middleware.conf['auth_admin_prefix'],
+ "/testadmin")
+ #ensure that signed requests do not generate HTTP traffic
+ self.assertEqual('', FakeHTTPConnection.last_requested_url)
+
+ def assert_unscoped_default_tenant_auto_scopes(self, token):
+ """Unscoped requests with a default tenant should "auto-scope."
+
+ The implied scope is the user's tenant ID.
+
+ """
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = token
+ body = self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.response_status, 200)
+ self.assertEqual(body, ['SUCCESS'])
+
+ def test_default_tenant_uuid_token(self):
+ self.assert_unscoped_default_tenant_auto_scopes(UUID_TOKEN_DEFAULT)
+
+ def test_default_tenant_signed_token(self):
+ self.assert_unscoped_default_tenant_auto_scopes(SIGNED_TOKEN_SCOPED)
+
+ def assert_unscoped_token_receives_401(self, token):
+ """Unscoped requests with no default tenant ID should be rejected."""
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = token
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.response_status, 401)
+ self.assertEqual(self.response_headers['WWW-Authenticate'],
+ 'Keystone uri=\'https://keystone.example.com:1234\'')
+
+ def test_unscoped_uuid_token_receives_401(self):
+ self.assert_unscoped_token_receives_401(UUID_TOKEN_UNSCOPED)
+
+ def test_unscoped_pki_token_receives_401(self):
+ self.assert_unscoped_token_receives_401(SIGNED_TOKEN_UNSCOPED)
+
+ def test_revoked_token_receives_401(self):
+ self.middleware.token_revocation_list = self.get_revocation_list_json()
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = REVOKED_TOKEN
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.response_status, 401)
+
+ def get_revocation_list_json(self, token_ids=None):
+ if token_ids is None:
+ token_ids = [REVOKED_TOKEN_HASH]
+ revocation_list = {'revoked': [{'id': x, 'expires': timeutils.utcnow()}
+ for x in token_ids]}
+ return jsonutils.dumps(revocation_list)
+
+ def test_is_signed_token_revoked_returns_false(self):
+ #explicitly setting an empty revocation list here to document intent
+ self.middleware.token_revocation_list = jsonutils.dumps(
+ {"revoked": [], "extra": "success"})
+ result = self.middleware.is_signed_token_revoked(REVOKED_TOKEN)
+ self.assertFalse(result)
+
+ def test_is_signed_token_revoked_returns_true(self):
+ self.middleware.token_revocation_list = self.get_revocation_list_json()
+ result = self.middleware.is_signed_token_revoked(REVOKED_TOKEN)
+ self.assertTrue(result)
+
+ def test_verify_signed_token_raises_exception_for_revoked_token(self):
+ self.middleware.token_revocation_list = self.get_revocation_list_json()
+ with self.assertRaises(auth_token.InvalidUserToken):
+ self.middleware.verify_signed_token(REVOKED_TOKEN)
+
+ def test_verify_signed_token_succeeds_for_unrevoked_token(self):
+ self.middleware.token_revocation_list = self.get_revocation_list_json()
+ self.middleware.verify_signed_token(SIGNED_TOKEN_SCOPED)
+
+ def test_get_token_revocation_list_fetched_time_returns_min(self):
+ self.middleware.token_revocation_list_fetched_time = None
+ self.middleware.revoked_file_name = ''
+ self.assertEqual(self.middleware.token_revocation_list_fetched_time,
+ datetime.datetime.min)
+
+ def test_get_token_revocation_list_fetched_time_returns_mtime(self):
+ self.middleware.token_revocation_list_fetched_time = None
+ mtime = os.path.getmtime(self.middleware.revoked_file_name)
+ fetched_time = datetime.datetime.fromtimestamp(mtime)
+ self.assertEqual(self.middleware.token_revocation_list_fetched_time,
+ fetched_time)
+
+ def test_get_token_revocation_list_fetched_time_returns_value(self):
+ expected = self.middleware._token_revocation_list_fetched_time
+ self.assertEqual(self.middleware.token_revocation_list_fetched_time,
+ expected)
+
+ def test_get_revocation_list_returns_fetched_list(self):
+ self.middleware.token_revocation_list_fetched_time = None
+ os.remove(self.middleware.revoked_file_name)
+ self.assertEqual(self.middleware.token_revocation_list,
+ REVOCATION_LIST)
+
+ def test_get_revocation_list_returns_current_list_from_memory(self):
+ self.assertEqual(self.middleware.token_revocation_list,
+ self.middleware._token_revocation_list)
+
+ def test_get_revocation_list_returns_current_list_from_disk(self):
+ in_memory_list = self.middleware.token_revocation_list
+ self.middleware._token_revocation_list = None
+ self.assertEqual(self.middleware.token_revocation_list, in_memory_list)
+
+ def test_invalid_revocation_list_raises_service_error(self):
+ globals()['SIGNED_REVOCATION_LIST'] = "{}"
+ with self.assertRaises(auth_token.ServiceError):
+ self.middleware.fetch_revocation_list()
+
+ def test_fetch_revocation_list(self):
+ fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list())
+ self.assertEqual(fetched_list, REVOCATION_LIST)
+
+ def test_request_invalid_uuid_token(self):
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = 'invalid-token'
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.response_status, 401)
+ self.assertEqual(self.response_headers['WWW-Authenticate'],
+ 'Keystone uri=\'https://keystone.example.com:1234\'')
+
+ def test_request_invalid_signed_token(self):
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = INVALID_SIGNED_TOKEN
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.response_status, 401)
+ self.assertEqual(self.response_headers['WWW-Authenticate'],
+ 'Keystone uri=\'https://keystone.example.com:1234\'')
+
+ def test_request_no_token(self):
+ req = webob.Request.blank('/')
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.response_status, 401)
+ self.assertEqual(self.response_headers['WWW-Authenticate'],
+ 'Keystone uri=\'https://keystone.example.com:1234\'')
+
+ def test_request_blank_token(self):
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = ''
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.response_status, 401)
+ self.assertEqual(self.response_headers['WWW-Authenticate'],
+ 'Keystone uri=\'https://keystone.example.com:1234\'')
+
+ def test_memcache(self):
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED
+ self.middleware._cache = FakeMemcache()
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.middleware._cache.set_value, None)
+
+ def test_memcache_set_invalid(self):
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = 'invalid-token'
+ self.middleware._cache = FakeMemcache()
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.middleware._cache.set_value, "invalid")
+
+ def test_memcache_set_expired(self):
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED
+ self.middleware._cache = FakeMemcache()
+ expired = datetime.datetime.now() - datetime.timedelta(minutes=1)
+ self.middleware._cache.token_expiration = float(expired.strftime("%s"))
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(len(self.middleware._cache.set_value), 2)
+
+ def test_nomemcache(self):
+ self.disable_module('memcache')
+
+ conf = {
+ 'admin_token': 'admin_token1',
+ 'auth_host': 'keystone.example.com',
+ 'auth_port': 1234,
+ 'memcache_servers': 'localhost:11211',
+ }
+
+ auth_token.AuthProtocol(FakeApp(), conf)
+
+ def test_request_prevent_service_catalog_injection(self):
+ req = webob.Request.blank('/')
+ req.headers['X-Service-Catalog'] = '[]'
+ req.headers['X-Auth-Token'] = UUID_TOKEN_NO_SERVICE_CATALOG
+ body = self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.response_status, 200)
+ self.assertFalse(req.headers.get('X-Service-Catalog'))
+ self.assertEqual(body, ['SUCCESS'])
+
+ def test_will_expire_soon(self):
+ tenseconds = datetime.datetime.utcnow() + datetime.timedelta(
+ seconds=10)
+ self.assertTrue(auth_token.will_expire_soon(tenseconds))
+ fortyseconds = datetime.datetime.utcnow() + datetime.timedelta(
+ seconds=40)
+ self.assertFalse(auth_token.will_expire_soon(fortyseconds))
diff --git a/tools/test-requires b/tools/test-requires
index 59afe9d..8d9547b 100644
--- a/tools/test-requires
+++ b/tools/test-requires
@@ -10,5 +10,7 @@ nosehtmloutput
pep8==1.2
sphinx>=1.1.2
unittest2>=0.5.1
+WebOb==1.0.8
+iso8601>=0.1.4
Babel