summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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