From 112f06fd9377d4e6964f8e0ee447f60267920cc0 Mon Sep 17 00:00:00 2001 From: Sai Krishna Date: Thu, 28 Apr 2011 17:13:27 +0530 Subject: Adding First kestone repo --- .gitignore | 1 + HACKING | 68 + LICENSE | 202 +++ README.md | 162 +++ docs/guide/pom.xml | 99 ++ docs/guide/src/docbkx/idm.wadl | 355 +++++ docs/guide/src/docbkx/idmdevguide.xml | 1018 +++++++++++++++ docs/guide/src/docbkx/img/Check_mark_23x20_02.svg | 60 + docs/guide/src/docbkx/samples/auth.json | 19 + docs/guide/src/docbkx/samples/auth.xml | 10 + .../guide/src/docbkx/samples/auth_credentials.json | 7 + docs/guide/src/docbkx/samples/auth_credentials.xml | 5 + docs/guide/src/docbkx/samples/choices.json | 50 + docs/guide/src/docbkx/samples/choices.xml | 29 + docs/guide/src/docbkx/samples/ext-getuser.json | 21 + docs/guide/src/docbkx/samples/ext-getuser.xml | 13 + docs/guide/src/docbkx/samples/extension.json | 21 + docs/guide/src/docbkx/samples/extension.xml | 23 + docs/guide/src/docbkx/samples/extensions.json | 44 + docs/guide/src/docbkx/samples/extensions.xml | 39 + docs/guide/src/docbkx/samples/getuser-1.json | 29 + docs/guide/src/docbkx/samples/getuser-1.xml | 14 + docs/guide/src/docbkx/samples/idm_fault.json | 7 + docs/guide/src/docbkx/samples/idm_fault.xml | 6 + docs/guide/src/docbkx/samples/item_not_found.json | 7 + docs/guide/src/docbkx/samples/item_not_found.xml | 6 + .../src/docbkx/samples/samplerequestheader.json | 4 + .../src/docbkx/samples/sampleresponseheader.json | 5 + docs/guide/src/docbkx/samples/tenant.json | 7 + docs/guide/src/docbkx/samples/tenant.xml | 5 + docs/guide/src/docbkx/samples/tenantlock.json | 5 + docs/guide/src/docbkx/samples/tenantlock.xml | 4 + docs/guide/src/docbkx/samples/tenants-1.json | 16 + docs/guide/src/docbkx/samples/tenants-1.xml | 10 + docs/guide/src/docbkx/samples/tenants-2.json | 20 + docs/guide/src/docbkx/samples/tenants-2.xml | 13 + docs/guide/src/docbkx/samples/tenants-3.json | 16 + docs/guide/src/docbkx/samples/tenants-3.xml | 10 + docs/guide/src/docbkx/samples/tenants.json | 15 + docs/guide/src/docbkx/samples/tenants.xml | 9 + docs/guide/src/docbkx/samples/updatedtenant.json | 7 + docs/guide/src/docbkx/samples/updatedtenant.xml | 5 + docs/guide/src/docbkx/samples/validatetoken.json | 19 + docs/guide/src/docbkx/samples/validatetoken.xml | 10 + docs/guide/src/docbkx/samples/version-atom.xml | 19 + docs/guide/src/docbkx/samples/version.json | 33 + docs/guide/src/docbkx/samples/version.xml | 23 + docs/guide/src/docbkx/samples/versions-atom.xml | 22 + docs/guide/src/docbkx/samples/versions.json | 28 + docs/guide/src/docbkx/samples/versions.xml | 18 + docs/guide/src/docbkx/xsd/api-common.xsd | 56 + docs/guide/src/docbkx/xsd/api.xsd | 14 + docs/guide/src/docbkx/xsd/atom/atom.xsd | 115 ++ docs/guide/src/docbkx/xsd/atom/xml.xsd | 287 +++++ docs/guide/src/docbkx/xsd/extensions.xsd | 56 + docs/guide/src/docbkx/xsd/fault.xsd | 135 ++ docs/guide/src/docbkx/xsd/tenant.xsd | 40 + docs/guide/src/docbkx/xsd/token.xsd | 72 ++ docs/guide/src/docbkx/xsd/version.xsd | 200 +++ echo/echo/__init__.py | 1 + echo/echo/echo.ini | 36 + echo/echo/echo.py | 121 ++ echo/echo/echo.wadl | 86 ++ echo/echo/echo_remote.ini | 19 + echo/echo/samples/echo.json | 11 + echo/echo/samples/echo.xml | 8 + echo/echo/xsd/echo.xsd | 48 + echo/echo/xsl/echo.xsl | 41 + echo/echo_client.py | 72 ++ echo/setup.py | 39 + keystone/__init__.py | 15 + keystone/auth_protocols/__init__.py | 0 keystone/auth_protocols/auth_basic.py | 114 ++ keystone/auth_protocols/auth_openid.py | 113 ++ keystone/auth_protocols/auth_token.ini | 18 + keystone/auth_protocols/auth_token.py | 265 ++++ keystone/common/__init__.py | 0 keystone/common/bufferedhttp.py | 165 +++ keystone/content/extensions.json | 1 + keystone/content/extensions.xml | 5 + keystone/content/idmdevguide.pdf | Bin 0 -> 208102 bytes keystone/content/version.json.tpl | 33 + keystone/content/version.xml.tpl | 23 + keystone/db/__init__.py | 0 keystone/db/sqlalchemy/__init__.py | 24 + keystone/db/sqlalchemy/api.py | 179 +++ keystone/db/sqlalchemy/models.py | 135 ++ keystone/db/sqlalchemy/session.py | 64 + keystone/identity.py | 302 +++++ keystone/identity.wadl | 1 + keystone/keystone.ini | 16 + keystone/logic/__init__.py | 0 keystone/logic/service.py | 216 ++++ keystone/logic/types/__init__.py | 0 keystone/logic/types/atom.py | 25 + keystone/logic/types/auth.py | 157 +++ keystone/logic/types/fault.py | 121 ++ keystone/logic/types/tenant.py | 119 ++ keystone/middleware/__init__.py | 0 keystone/middleware/remoteauth.py | 105 ++ keystone/samples | 1 + keystone/xsd | 1 + management/delgroup.py | 42 + management/getgroup.py | 40 + management/getgroups.py | 37 + management/getgroupusers.py | 39 + management/getuser.py | 37 + management/getusergroups.py | 38 + management/getusers.py | 38 + management/groupadd.py | 40 + management/setuserlock.py | 49 + management/setuserpswd.py | 41 + management/updategroup.py | 42 + management/useradd.py | 42 + management/userdel.py | 39 + management/userupdate.py | 41 + pip-requires | 9 + setup.py | 42 + test | 1 - test/EchoSOAPUI.xml | 2 + test/IdentitySOAPUI.xml | 1355 ++++++++++++++++++++ test/kill.sql | 10 + test/test_setup.sql | 71 + test/unit/test_identity.py | 966 ++++++++++++++ 124 files changed, 9333 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 HACKING create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/guide/pom.xml create mode 100644 docs/guide/src/docbkx/idm.wadl create mode 100644 docs/guide/src/docbkx/idmdevguide.xml create mode 100644 docs/guide/src/docbkx/img/Check_mark_23x20_02.svg create mode 100644 docs/guide/src/docbkx/samples/auth.json create mode 100644 docs/guide/src/docbkx/samples/auth.xml create mode 100644 docs/guide/src/docbkx/samples/auth_credentials.json create mode 100644 docs/guide/src/docbkx/samples/auth_credentials.xml create mode 100644 docs/guide/src/docbkx/samples/choices.json create mode 100644 docs/guide/src/docbkx/samples/choices.xml create mode 100644 docs/guide/src/docbkx/samples/ext-getuser.json create mode 100644 docs/guide/src/docbkx/samples/ext-getuser.xml create mode 100644 docs/guide/src/docbkx/samples/extension.json create mode 100644 docs/guide/src/docbkx/samples/extension.xml create mode 100644 docs/guide/src/docbkx/samples/extensions.json create mode 100644 docs/guide/src/docbkx/samples/extensions.xml create mode 100644 docs/guide/src/docbkx/samples/getuser-1.json create mode 100644 docs/guide/src/docbkx/samples/getuser-1.xml create mode 100644 docs/guide/src/docbkx/samples/idm_fault.json create mode 100644 docs/guide/src/docbkx/samples/idm_fault.xml create mode 100644 docs/guide/src/docbkx/samples/item_not_found.json create mode 100644 docs/guide/src/docbkx/samples/item_not_found.xml create mode 100644 docs/guide/src/docbkx/samples/samplerequestheader.json create mode 100644 docs/guide/src/docbkx/samples/sampleresponseheader.json create mode 100644 docs/guide/src/docbkx/samples/tenant.json create mode 100644 docs/guide/src/docbkx/samples/tenant.xml create mode 100644 docs/guide/src/docbkx/samples/tenantlock.json create mode 100644 docs/guide/src/docbkx/samples/tenantlock.xml create mode 100644 docs/guide/src/docbkx/samples/tenants-1.json create mode 100644 docs/guide/src/docbkx/samples/tenants-1.xml create mode 100644 docs/guide/src/docbkx/samples/tenants-2.json create mode 100644 docs/guide/src/docbkx/samples/tenants-2.xml create mode 100644 docs/guide/src/docbkx/samples/tenants-3.json create mode 100644 docs/guide/src/docbkx/samples/tenants-3.xml create mode 100644 docs/guide/src/docbkx/samples/tenants.json create mode 100644 docs/guide/src/docbkx/samples/tenants.xml create mode 100644 docs/guide/src/docbkx/samples/updatedtenant.json create mode 100644 docs/guide/src/docbkx/samples/updatedtenant.xml create mode 100644 docs/guide/src/docbkx/samples/validatetoken.json create mode 100644 docs/guide/src/docbkx/samples/validatetoken.xml create mode 100644 docs/guide/src/docbkx/samples/version-atom.xml create mode 100644 docs/guide/src/docbkx/samples/version.json create mode 100644 docs/guide/src/docbkx/samples/version.xml create mode 100644 docs/guide/src/docbkx/samples/versions-atom.xml create mode 100644 docs/guide/src/docbkx/samples/versions.json create mode 100644 docs/guide/src/docbkx/samples/versions.xml create mode 100644 docs/guide/src/docbkx/xsd/api-common.xsd create mode 100644 docs/guide/src/docbkx/xsd/api.xsd create mode 100644 docs/guide/src/docbkx/xsd/atom/atom.xsd create mode 100644 docs/guide/src/docbkx/xsd/atom/xml.xsd create mode 100644 docs/guide/src/docbkx/xsd/extensions.xsd create mode 100644 docs/guide/src/docbkx/xsd/fault.xsd create mode 100644 docs/guide/src/docbkx/xsd/tenant.xsd create mode 100644 docs/guide/src/docbkx/xsd/token.xsd create mode 100644 docs/guide/src/docbkx/xsd/version.xsd create mode 100644 echo/echo/__init__.py create mode 100644 echo/echo/echo.ini create mode 100644 echo/echo/echo.py create mode 100644 echo/echo/echo.wadl create mode 100644 echo/echo/echo_remote.ini create mode 100644 echo/echo/samples/echo.json create mode 100644 echo/echo/samples/echo.xml create mode 100644 echo/echo/xsd/echo.xsd create mode 100644 echo/echo/xsl/echo.xsl create mode 100644 echo/echo_client.py create mode 100644 echo/setup.py create mode 100644 keystone/__init__.py create mode 100644 keystone/auth_protocols/__init__.py create mode 100644 keystone/auth_protocols/auth_basic.py create mode 100644 keystone/auth_protocols/auth_openid.py create mode 100644 keystone/auth_protocols/auth_token.ini create mode 100644 keystone/auth_protocols/auth_token.py create mode 100644 keystone/common/__init__.py create mode 100644 keystone/common/bufferedhttp.py create mode 100644 keystone/content/extensions.json create mode 100644 keystone/content/extensions.xml create mode 100644 keystone/content/idmdevguide.pdf create mode 100644 keystone/content/version.json.tpl create mode 100644 keystone/content/version.xml.tpl create mode 100644 keystone/db/__init__.py create mode 100644 keystone/db/sqlalchemy/__init__.py create mode 100644 keystone/db/sqlalchemy/api.py create mode 100644 keystone/db/sqlalchemy/models.py create mode 100644 keystone/db/sqlalchemy/session.py create mode 100644 keystone/identity.py create mode 120000 keystone/identity.wadl create mode 100644 keystone/keystone.ini create mode 100644 keystone/logic/__init__.py create mode 100644 keystone/logic/service.py create mode 100644 keystone/logic/types/__init__.py create mode 100644 keystone/logic/types/atom.py create mode 100644 keystone/logic/types/auth.py create mode 100644 keystone/logic/types/fault.py create mode 100644 keystone/logic/types/tenant.py create mode 100644 keystone/middleware/__init__.py create mode 100644 keystone/middleware/remoteauth.py create mode 120000 keystone/samples create mode 120000 keystone/xsd create mode 100644 management/delgroup.py create mode 100644 management/getgroup.py create mode 100644 management/getgroups.py create mode 100644 management/getgroupusers.py create mode 100644 management/getuser.py create mode 100644 management/getusergroups.py create mode 100644 management/getusers.py create mode 100644 management/groupadd.py create mode 100644 management/setuserlock.py create mode 100644 management/setuserpswd.py create mode 100644 management/updategroup.py create mode 100644 management/useradd.py create mode 100644 management/userdel.py create mode 100644 management/userupdate.py create mode 100644 pip-requires create mode 100644 setup.py delete mode 100644 test create mode 100644 test/EchoSOAPUI.xml create mode 100644 test/IdentitySOAPUI.xml create mode 100644 test/kill.sql create mode 100644 test/test_setup.sql create mode 100644 test/unit/test_identity.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0d20b648 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/HACKING b/HACKING new file mode 100644 index 00000000..e58d60e5 --- /dev/null +++ b/HACKING @@ -0,0 +1,68 @@ +Nova Style Commandments +======================= + +Step 1: Read http://www.python.org/dev/peps/pep-0008/ +Step 2: Read http://www.python.org/dev/peps/pep-0008/ again +Step 3: Read on + +Imports +------- +- thou shalt not import objects, only modules +- thou shalt not import more than one module per line +- thou shalt not make relative imports +- thou shalt organize your imports according to the following template + +:: + # vim: tabstop=4 shiftwidth=4 softtabstop=4 + {{stdlib imports in human alphabetical order}} + \n + {{nova imports in human alphabetical order}} + \n + \n + {{begin your code}} + + +General +------- +- thou shalt put two newlines twixt toplevel code (funcs, classes, etc) +- thou shalt put one newline twixt methods in classes and anywhere else +- thou shalt not write "except:", use "except Exception:" at the very least +- thou shalt include your name with TODOs as in "TODO(termie)" +- thou shalt not name anything the same name as a builtin or reserved word +- thou shalt not violate causality in our time cone, or else + + +Human Alphabetical Order Examples +--------------------------------- +:: + import httplib + import logging + import random + import StringIO + import time + import unittest + + from nova import flags + from nova import test + from nova.auth import users + from nova.endpoint import api + from nova.endpoint import cloud + +Docstrings +---------- + """Summary of the function, class or method, less than 80 characters. + + New paragraph after newline that explains in more detail any general + information about the function, class or method. After this, if defining + parameters and return types use the Sphinx format. After that an extra + newline then close the quotations. + + When writing the docstring for a class, an extra line should be placed + after the closing quotations. For more in-depth explanations for these + decisions see http://www.python.org/dev/peps/pep-0257/ + + :param foo: the foo parameter + :param bar: the bar parameter + :returns: description of the return value + + """ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..75b52484 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..3753b402 --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +Keystone: Identity Service +========================== + +Keystone is a proposed independent authentication service for [OpenStack](http://www.openstack.org). + +This initial proof of concept aims to address the current use cases in Swift and Nova which are: + +* REST-based, token auth for Swift +* many-to-many relationship between identity and tenant for Nova. + + +SERVICES: +--------- + +* Keystone - authentication service +* Auth_Token - WSGI middleware that can be used to handle token auth protocol (WSGI or remote proxy) +* Echo - A sample service that responds by returning call details + +Also included: +* Auth_Basic - Stub for WSGI middleware that will be used to handle basic auth +* Auth_OpenID - Stub for WSGI middleware that will be used to handle openid auth protocol +* RemoteAuth - WSGI middleware that can be used in services (like Swift, Nova, and Glance) when Auth middleware is running remotely + + +DEPENDENCIES: +------------- + +* bottle +* eventlet +* lxml +* Paste +* PasteDeploy +* PasteScript +* SQLAlchemy +* SQLite3 +* webob + + +SETUP: +------ + +Install http://pypi.python.org/pypi/setuptools + + sudo easy_install bottle + sudo easy_install eventlet + sudo easy_install lxml + sudo easy_install paste + sudo easy_install pastedeploy + sudo easy_install pastescript + sudo easy_install pysqlite + sudo easy_install sqlalchemy + sudo easy_install webob + +Or using pip: + + sudo pip install -r pip-requires + + +RUNNING KEYSTONE: +----------------- + + $ cd keystone + $ python identity.py + + +RUNNING TEST SERVICE: +--------------------- + + Standalone stack (with Auth_Token) + $ cd echo/echo + $ python echo.py + + Distributed stack (with RemoteAuth local and Auth_Token remote) + $ cd echo/echo + $ python echo.py --remote + + in separate session + $ cd keystone/auth_protocols + $ python auth_token.py --remote + +DEMO CLIENT: +--------------------- + $ cd echo/echo + $ python echo_client.py + + +INSTALLING KEYSTONE: +-------------------- + + $ python setup.py build + $ sudo python setup.py install + + +INSTALLING TEST SERVICE: +------------------------ + + $ cd echo + $ python setup.py build + $ sudo python setup.py install + + +TESTING +------- + +After starting identity.py a keystone.db sql-lite database should be created. + +To test setup the test database: + + $ sqlite3 keystone/keystone.db < test/test_setup.sql + +To clean the test database + + $ sqlite3 keystone/keystone.db < test/kill.sql + +To run unit tests: + + $ python test/unit/test_identity.py + +To run client demo (with all auth middleware running locally on sample service): + + $ python echo/echo/echo.py + $ python echo/echo/echo_client.py + + +To perform contract validation and load testing, use SoapUI (for now). + +Using SOAPUI: + +Download [SOAPUI](http://sourceforge.net/projects/soapui/files/): + +To Test Identity Service: + +* File->Import Project +* Select tests/IdentitySOAPUI.xml +* Double click on "Keystone Tests" and press the green play (>) button + + +Unit Test on Identity Services +------------------------------ +In order to run the unit test on identity services, run from the keystone directory + + python identity.py + +Once the Identity service is running, go to unit test/unit directory + + python test_identity.py + +For more on unit testing please refer + + python test_identity --help + + + +DATABASE SCHEMA +--------------- + + CREATE TABLE groups(group_id varchar(255),group_desc varchar(255),tenant_id varchar(255),FOREIGN KEY(tenant_id) REFERENCES tenant(tenant_id)); + CREATE TABLE tenants(tenant_id varchar(255), tenant_desc varchar(255), tenant_enabled INTEGER, PRIMARY KEY(tenant_id ASC)); + CREATE TABLE token(token_id varchar(255),user_id varchar(255),expires datetime,tenant_id varchar(255)); + CREATE TABLE user_group(user_id varchar(255),group_id varchar(255), FOREIGN KEY(user_id) REFERENCES user(id), FOREIGN KEY(group_id) REFERENCES groups(group_id)); + CREATE TABLE user_tenant(tenant_id varchar(255),user_id varchar(255),FOREIGN KEY(tenant_id) REFERENCES tenant(tenant_id),FOREIGN KEY(user_id) REFERENCES user(id)); + CREATE TABLE users(id varchar(255),password varchar(255),email varchar(255),enabled integer); diff --git a/docs/guide/pom.xml b/docs/guide/pom.xml new file mode 100644 index 00000000..ec944cf4 --- /dev/null +++ b/docs/guide/pom.xml @@ -0,0 +1,99 @@ + + 4.0.0 + + com.rackspace.idm + docs + 0.0.1-SNAPSHOT + jar + + docs + http://maven.apache.org + + + UTF-8 + + + + + junit + junit + 3.8.1 + test + + + com.sun.xml.bind + jaxb-impl + 2.1.12 + + + com.thoughtworks.xstream + xstream + 1.3.1 + + + org.codehaus.jettison + jettison + 1.1 + + + + + + target/docbkx/pdf + + **/*.fo + + + + + + com.rackspace.cloud.api + clouddocs-maven-plugin + 1.0.4-SNAPSHOT + + + + generate-pdf + generate-webhelp + + generate-sources + + + + + org.docbook + docbook-xml + 4.4 + runtime + + + + true + + + + + + + Rackspace Research Repositories + + true + + + + rackspace-research + Rackspace Research Repository + http://maven.research.rackspacecloud.com/content/groups/public/ + + + + + rackspace-research + Rackspace Research Repository + http://maven.research.rackspacecloud.com/content/groups/public/ + + + + + diff --git a/docs/guide/src/docbkx/idm.wadl b/docs/guide/src/docbkx/idm.wadl new file mode 100644 index 00000000..39135b71 --- /dev/null +++ b/docs/guide/src/docbkx/idm.wadl @@ -0,0 +1,355 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/guide/src/docbkx/idmdevguide.xml b/docs/guide/src/docbkx/idmdevguide.xml new file mode 100644 index 00000000..4b279cef --- /dev/null +++ b/docs/guide/src/docbkx/idmdevguide.xml @@ -0,0 +1,1018 @@ + + + + + + + + GET'> + PUT'> + POST'> + DELETE'> + + + + + + + '> + + + + This operation does not require a request body.'> + + + Verb + URI + Description + + '> + + + Verb + URI + Description + + '> +]> + + Cloud Identity Developer Guide + + + + + + + + Rackspace Cloud + + + + 2010 + 2011 + Rackspace Hosting, Inc. + + API v1.0 + Cloud Identity + 2011-04-23 + + + Copyright details are filled in by the template. + + + + + This document is intended for software developers interested + in developing applications which utilize the Cloud Identity + Service for authentication. This document also includes + details on how to integrate services with the Cloud Identity + Service. + + + + + Overview + + The Cloud Identity Service allows applications to obtain + tokens that can be used to access OpenStack resources. This + document is intended for software developers interested in + developing applications which utilize the Cloud Identity + Service for authentication. This document also includes + details on how to integrate services with the Cloud Identity + Service. + + + This Guide assumes the reader is familiar with RESTful web + services, HTTP/1.1, and JSON and/or XML serialization formats. + + + + Concepts + + The Cloud Identity Service has several key concepts that are + important to understand: + +
+ Token + + A token is an arbitrary bit of text that is used to access + resources. Each token has a scope which describes which + resources are accessible with it. A token may be + revoked at anytime and is valid for a finite duration. + +
+
+ Tenant + + Depending on the operator, a tenant may map to a customer, + account, organization, or project. + +
+
+ User + + Users have a login and may be assigned tokens to access + resources. + +
+
+ Group + + A group of users. Global groups are managed by + operators. They are used to organize and assign privileges + to a group of related users. For example, an operator may + create a "delinquent" group, which will assign limited + privileges to users who have past due bills. + +
+
+ + General API Information + The IdM API is implemented using a RESTful web service interface. All requests to + authenticate and operate against the IdM API are performed using SSL over HTTP (HTTPS) on TCP + port 443. +
+ Request/Response Types + The IdM API supports both the JSON and XML data serialization formats. The request + format is specified using the Content-Type header and is required for + operations that have a request body. The response format can be specified in requests using + either the Accept header or adding an .xml or .json + extension to the request URI. Note that it is possible for a response to be serialized using + a format different from the request (see example below). If no response format is specified, + JSON is the default. If conflicting formats are specified using both an Accept + header and a query extension, the query extension takes precedence. + + + + + + + + + + + + + + + + + + + + + + + + +
Response Types
FormatAccept HeaderQuery ExtensionDefault
JSONapplication/json.jsonYes
XMLapplication/xml.xmlNo
+ + JSON Request with Headers + + + + + + + + + XML Response with Headers + + + + + + + +
+
+ Content Compression + Request and response body data my be encoded with gzip compression in order to + accelerate interactive performance of API calls and responses. This is controlled using the + Accept-Encoding header on the request from the client and indicated by the + Content-Encoding header in the server response. Unless the header is + explicitly set, encoding defaults to disabled. + + + + + + + + + + + + + + + + + + + + + +
Compression Headers
Header TypeNameValue
HTTP/1.1 RequestAccept-Encodinggzip
HTTP/1.1 ResponseContent-Encodinggzip
+
+
+ Paginated Collections + + To reduce load on the service, list operations will + return a maximum number of items at a time. The + maximum number of items returned is determined by the + IDM provider. To navigate the collection, the + parameters limit and + marker can be set in the URI + (e.g.?limit=100&marker=1234). + The marker parameter is the ID + of the last item in the previous list. Items are + sorted by update time. When an update time is not + available they are sorted by ID. The + limit parameter sets the page + size. Both parameters are optional. If the client + requests a limit beyond that + which is supported by the deployment an overLimit + (413) fault may be thrown. A + marker with an invalid ID will return an itemNotFound + (404) fault. + + + + Paginated collections never return itemNotFound + (404) faults when the + collection is empty — clients should expect + an empty collection. + + + + For convenience, collections contain atom "next" and + "previous" links. The first page in the list will not + contain a "previous" link, the last page in the list + will not contain a "next" link. The following examples + illustrate three pages in a collection of tenants. The + first page was retrieved via a &GET; to + http://idm.api.openstack.org/v1.0/1234/tenants?limit=1. + In these examples, the limit + parameter sets the page size to a single item. + Subsequent "next" and "previous" links will honor the + initial page size. Thus, a client may follow links to + traverse a paginated collection without having to + input the marker parameter. + + + Tenant Collection, First Page: XML + + + + + + Tenant Collection, First Page: JSON + + + + Tenant Collection, Second Page: XML + + + + + + Tenant Collection, Second Page: JSON + + + + Tenant Collection, Last Page: XML + + + + + + Tenant Collection, Last Page: JSON + + + + In the JSON representation, paginated collections contain + a values property that contains the + items in the collections. Links are accessed via the + links property. The approach allows + for extensibility of both the collection members and of + the paginated collection itself. It also allows + collections to be embedded in other objects as illustrated + below. Here, a subset of grups are presented within a + user. Clients must follow the "next" link to continue to + retrive additonal groups belonging to a user. + + + Paginated Groups in a User: XML + + + + + + Paginated Groups in an User: JSON + + +
+
+ Versions + + The OpenStack IDM API uses both a URI and a MIME + type versioning scheme. In the URI scheme, the first + element of the path contains the target version + identifier (e.g. https://idm.api.openstack.org/ + v1.0/…). The MIME type versioning scheme uses + HTTP content negotiation where the Accept + or Content-Type headers contains a MIME + type that identifies the version + (application/vnd.openstack.idm-v1.1+xml). A + version MIME type is always linked to a base MIME type + (application/xml or application/json). If conflicting + versions are specified using both an HTTP header and a + URI, the URI takes precedence. + + + Request with MIME type versioning + +GET /tenants HTTP/1.1 +Host: idm.api.openstack.org +Accept: application/vnd.openstack.idm-v1.1+xml +X-Auth-Token: eaaafd18-0fed-4b3a-81b4-663c99ec1cbb + + + + Request with URI versioning + +GET /v1.1/tenants HTTP/1.1 +Host: idm.api.openstack.org +Accept: application/xml +X-Auth-Token: eaaafd18-0fed-4b3a-81b4-663c99ec1cbb + + + + + The MIME type versioning approach allows for the + creating of permanent links, because the version + scheme is not specified in the URI path: + https://api.idm.openstack.org/tenants/12234. + + + + If a request is made without a version specified in + the URI or via HTTP headers, then a multiple-choices + response (300) will follow + providing links and MIME types to available versions. + + + Multiple Choices Response: XML + + + + + + Multiple Choices Response: JSON + + + + New features and functionality that do not break + API-compatibility will be introduced in the current + version of the API as extensions (see below) and the + URI and MIME types will remain unchanged. Features or + functionality changes that would necessitate a break + in API-compatibility will require a new version, which + will result in URI and MIME type version being updated + accordingly. When new API versions are released, older + versions will be marked as + DEPRECATED. Providers should work with + developers and partners to ensure there is adequate + time to migrate to the new version before deprecated + versions are discontinued. + + + Your application can programmatically determine + available API versions by performing a &GET; on the + root URL (i.e. with the version and everything to the + right of it truncated) returned from the + authentication system. Note that an Atom + representation of the versions resources is supported + when issuing a request with the Accept + header containing application/atom+xml or by adding a + .atom to the request URI. This allows standard Atom + clients to track version changes. + + + Versions List Request + +GET HTTP/1.1 +Host: idm.api.openstack.org + + + &CODES;200, 203 + &ERROR_CODES; badRequest + (400), idmFault + (500), + serviceUnavailable(503) + &NO_REQUEST; + + Versions List Response: XML + + + + + + Versions List Response: Atom + + + + + + Versions List Response: JSON + + + + You can also obtain additional information about a + specific version by performing a &GET; on the base + version URL + (e.g. https://idm.api.openstack.org/v1.1/). + Version request URLs should always end with a trailing + slash (/). If the slash is omitted, the server may + respond with a 302 + redirection request. Format extensions may be placed + after the slash + (e.g. https://idm.api.openstack.org/v1.1/.xml). Note + that this is a special case that does not hold true + for other API requests. In general, requests such as + /tenants.xml and /tenants/.xml are handled + equivalently. + + + Version Details Request + +GET HTTP/1.1 +Host: idm.api.openstack.org/v1.1/ + + + &CODES;200, 203 + &ERROR_CODES; badRequest + (400), idmFault + (500), + serviceUnavailable(503) + &NO_REQUEST; + + Version Details Response: XML + + + + + + Version Details Response: Atom + + + + + + Version Details Response: JSON + + + + The detailed version response contains pointers to + both a human-readable and a machine-processable + description of the API service. The machine-processable description is written in the Web + Application Description Language (WADL). + + + If there is a discrepancy between the two specifications, the WADL is + authoritative as it contains the most accurate and up-to-date description of the + API service. + +
+
+ Extensions + + The OpenStack IDM API is extensible. Extensions + serve two purposes: They allow the introduction of new + features in the API without requiring a version change + and they allow the introduction of vendor specific + niche functionality. Applications can programmatically + determine what extensions are available by performing + a &GET; on the /extensions URI. Note that this is a + versioned request — that is, an extension + available in one API version may not be available in + another. + + + + + Verb + URI + Description + + + + + &GET; + /extensions + Returns a list of available extensions + + + + &CODES;200, 203 + &ERROR_CODES; badRequest + (400), idmFault (500), + serviceUnavailable(503) + &NO_REQUEST; + + Each extension is identified by two unique identifiers, a + namespace and an + alias. Additionally an extension + contains documentation links in various formats. + + + Extensions Response: XML + + + + + + Extensions Response: JSON + + + + Extensions may also be queried individually by their + unique alias. This provides the simplest method of + checking if an extension is available as an unavailable + extension will issue an itemNotFound + (404) response. + + + + + Verb + URI + Description + + + + + &GET; + /extensions/alias + Return details of a single extension + + + + &CODES;200, 203 + &ERROR_CODES; itemNotFound + (404), badRequest + (400), idmFault + (500), + serviceUnavailable(503) + &NO_REQUEST; + + Extension Response: xml + + + + + + Extensions Response: JSON + + + + Extensions may define new data types, parameters, actions, + headers, states, and resources. In XML, additional + elements and attributes may be defined. These elements + must be defined in the extension's namespace. In JSON, the + alias must be used. The volumes element in the and + + is defined in the RS-META namespace. Extended + headers are always prefixed with X- followed + by the alias and a dash: (X-RS-META-HEADER1). + Parameters must be prefixed with the extension alias + followed by a colon. + + + + Applications should be prepared to ignore response + data that contains extension elements. Also, + applications should also verify that an extension is + available before submitting an extended request. + + + + Extended User Response: XML + + + + + + Extended User Response: JSON + + +
+
+ Faults + When an error occurs the system will return an HTTP error response code denoting the + type of error. The system will also return additional information about the fault in the + body of the response. + + XML Fault Response + + + + + + JSON Fault Response + + + + + The error code is returned in the body of the response for convenience. The message + section returns a human readable message. The details section is optional and may contain + useful information for tracking down an error (e.g a stack trace). + The root element of the fault (e.g. idmFault) may change depending on the type of error. + The following is an example of an itemNotFound error. + + XML Not Found Fault + + + + + + JSON Not Found Fault + + + + + The following is a list of possible fault types along with their associated error + codes. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fault Types
Fault ElementAssociated Error CodeExpected in All Requests
idmFault500, 400 &CHECK;
serviceUnavailable503 &CHECK;
badRequest400 &CHECK;
unauthorized401 &CHECK;
overLimit413 +
userDisabled403 +
forbidden403 +
itemNotFound404 +
tenantConflict409 +
+ From an XML schema perspective, all API faults are extensions of the base fault type + idmFault. When working with a system that binds XML to actual classes (such + as JAXB), one should be capable of using idmFault as a “catch-all” if + there's no interest in distinguishing between individual fault types. +
+
+ + Service Developer Operations +
+ Overview + The operations described in this chapter allow service developers to get and validate + access tokens, manage users, and manage tenants. +
+
+ Token Operations +
+ Authenticate + + &URI_REFHEAD; + + + &POST; + /token + Authenticate to generate a token. + + + + &CODES;200, 203 + &ERROR_CODES; unauthorized (401), userDisabled + (403), badRequest (400), idmFault + (500), serviceUnavailable(503) + + TenantID is optional and may be used to specify that a + token should be returned that has access for resources + that particular tenant. + + + XML Auth Request + + + + + + JSON Auth Request + + + + + + XML Auth Response + + + + + + JSON Auth Response + + + + +
+
+ Validate Token + + &LONG_URI_REFHEAD; + + + &GET; + /token/tokenId?belongsTo=tenantId + Check that a token is valid and that it belongs to a particular user + and return the permissions relevant to a particular client. + + + + &CODES;200, 203 + &ERROR_CODES; unauthorized (401), forbidden + (403), userDisabled(403), + badRequest (400), itemNotFound (404), + idmFault(500), + serviceUnavailable(503) + &NO_REQUEST; + + Valid tokens will exist in the + /token/tokenId path and invalid + tokens will not. In other words, a user should expect an + itemNotFound (404) fault for an + invalid token. + + + XML Validate Token Response + + + + + + JSON Validate Token Response + + + + +
+
+ Revoke Token + + &LONG_URI_REFHEAD; + + + &DELETE; + /token/tokenId + Revoke an existing token. + + + + &CODES;204 + &ERROR_CODES; unauthorized (401), forbidden + (403), userDisabled(403), + badRequest (400), itemNotFound (404), + idmFault(500), + serviceUnavailable(503) + &NO_REQUEST; +
+
+
+ Tenant Operations +
+ Create a Tenant + + &LONG_URI_REFHEAD; + + + &POST; + /tenants + Create a tenant + + + + &CODES;201 + &ERROR_CODES; unauthorized (401), + forbidden(403), + badRequest (400), idmFault (500), + serviceUnavailable(503) + + XML Tenant Create Request + + + + + + JSON Tenant Create Request + + + + + + XML Tenant Create Response + + + + + + JSON Tenant Create Response + + + + +
+
+ Get Tenants + + &LONG_URI_REFHEAD; + + + &GET; + /tenants + Get a list of tenants. + + + + &CODES;200, 203 + &ERROR_CODES; unauthorized (401), + forbidden(403), overLimit(413), + badRequest (400), idmFault (500), + serviceUnavailable(503) + + The operation returns a list of tenants. The list may be + filtered to return only those tenants which the caller has + access to. + + &NO_REQUEST; + + XML Tenants Response + + + + + + JSON Tenants Response + + + + +
+
+ Get a Tenant + + &LONG_URI_REFHEAD; + + + &GET; + /tenants/tenantId + Get a tenant. + + + + &CODES;200, 203 + &ERROR_CODES; unauthorized (401), + forbidden(403), itemNotFound(404), + badRequest (400), idmFault (500), + serviceUnavailable(503) + &NO_REQUEST; + + XML Tenant Response + + + + + + JSON Tenant Response + + + + +
+
+ Update a Tenant + + &LONG_URI_REFHEAD; + + + &PUT; + /tenants/tenantId + Update a tenant.. + + + + &CODES;200 + &ERROR_CODES; unauthorized (401), + forbidden(403), itemNotFound(404), + badRequest (400), idmFault (500), + serviceUnavailable(503) + + XML Tenant Update Request + + + + + + JSON Tenant Update Request + + + + + + XML Tenant Update Response + + + + + + JSON Tenant Update Response + + + + +
+
+ Delete a Tenant + + &LONG_URI_REFHEAD; + + + &DELETE; + /tenants/tenantId + Delete a Tenant. + + + + &CODES;204 + &ERROR_CODES; unauthorized (401), badRequest + (400), forbidden (403), itemNotFound + (404), idmFault (500), + serviceUnavailable(503) + &NO_REQUEST; +
+
+
+
diff --git a/docs/guide/src/docbkx/img/Check_mark_23x20_02.svg b/docs/guide/src/docbkx/img/Check_mark_23x20_02.svg new file mode 100644 index 00000000..3051a2f9 --- /dev/null +++ b/docs/guide/src/docbkx/img/Check_mark_23x20_02.svg @@ -0,0 +1,60 @@ + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/docs/guide/src/docbkx/samples/auth.json b/docs/guide/src/docbkx/samples/auth.json new file mode 100644 index 00000000..fa56c131 --- /dev/null +++ b/docs/guide/src/docbkx/samples/auth.json @@ -0,0 +1,19 @@ +{ + "auth" : { + "token": { + "id": "ab48a9efdfedb23ty3494", + "expires": "2010-11-01T03:32:15-05:00" + }, + "user" : { + "groups": { + "group": [ + { + "tenantId" : "1234", + "id": "Admin" + } + ]}, + "username": "jqsmith", + "tenantId": "1234" + } + } +} diff --git a/docs/guide/src/docbkx/samples/auth.xml b/docs/guide/src/docbkx/samples/auth.xml new file mode 100644 index 00000000..df8d69d0 --- /dev/null +++ b/docs/guide/src/docbkx/samples/auth.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/docs/guide/src/docbkx/samples/auth_credentials.json b/docs/guide/src/docbkx/samples/auth_credentials.json new file mode 100644 index 00000000..67b06304 --- /dev/null +++ b/docs/guide/src/docbkx/samples/auth_credentials.json @@ -0,0 +1,7 @@ +{ + "passwordCredentials" : { + "username" : "test_user", + "password" : "a86850deb2742ec3cb41518e26aa2d89", + "tenantId" : "77654" + } +} diff --git a/docs/guide/src/docbkx/samples/auth_credentials.xml b/docs/guide/src/docbkx/samples/auth_credentials.xml new file mode 100644 index 00000000..fbdf5633 --- /dev/null +++ b/docs/guide/src/docbkx/samples/auth_credentials.xml @@ -0,0 +1,5 @@ + + diff --git a/docs/guide/src/docbkx/samples/choices.json b/docs/guide/src/docbkx/samples/choices.json new file mode 100644 index 00000000..6b0b2607 --- /dev/null +++ b/docs/guide/src/docbkx/samples/choices.json @@ -0,0 +1,50 @@ +{ + "choices" : { + "values" : [ + { + "id" : "v1.0", + "status" : "DEPRECATED", + "links": [ + { + "rel" : "self", + "href" : "http://idm.api.openstack.org/v1.0/tenants/12" + } + ], + "media-types": { + "values" : [ + { + "base" : "application/xml", + "type" : "application/vnd.openstack.idm-v1.0+xml" + }, + { + "base" : "application/json", + "type" : "application/vnd.openstack.idm-v1.0+json" + } + ] + } + }, + { + "id" : "v1.1", + "status" : "CURRENT", + "links": [ + { + "rel" : "self", + "href" : "http://idm.api.openstack.org/v1.1/tenants/12" + } + ], + "media-types": { + "values" : [ + { + "base" : "application/xml", + "type" : "application/vnd.openstack.idm-v1.1+xml" + }, + { + "base" : "application/json", + "type" : "application/vnd.openstack.idm-v1.1+json" + } + ] + } + } + ] + } +} diff --git a/docs/guide/src/docbkx/samples/choices.xml b/docs/guide/src/docbkx/samples/choices.xml new file mode 100644 index 00000000..a9a91ece --- /dev/null +++ b/docs/guide/src/docbkx/samples/choices.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/guide/src/docbkx/samples/ext-getuser.json b/docs/guide/src/docbkx/samples/ext-getuser.json new file mode 100644 index 00000000..03f74dda --- /dev/null +++ b/docs/guide/src/docbkx/samples/ext-getuser.json @@ -0,0 +1,21 @@ +{"user": + { + "groups": { + "values": [ + { + "tenantId" : "1234", + "id": "Admin" + } + ]}, + "id": "jqsmith", + "tenantId": "1234", + "email": "john.smith@example.org", + "enabled": true, + "RS-META:metadata" : { + "values" : { + "MetaKey1" : "MetaValue1", + "MetaKey2" : "MetaValue2" + } + }, + } +} diff --git a/docs/guide/src/docbkx/samples/ext-getuser.xml b/docs/guide/src/docbkx/samples/ext-getuser.xml new file mode 100644 index 00000000..07c185b1 --- /dev/null +++ b/docs/guide/src/docbkx/samples/ext-getuser.xml @@ -0,0 +1,13 @@ + + + + + + + MetaValue1 + MetaValue2 + + diff --git a/docs/guide/src/docbkx/samples/extension.json b/docs/guide/src/docbkx/samples/extension.json new file mode 100644 index 00000000..1d7e8bb3 --- /dev/null +++ b/docs/guide/src/docbkx/samples/extension.json @@ -0,0 +1,21 @@ +{ + "extension" : { + "name" : "User Metadata Extension", + "namespace" : "http://docs.rackspacecloud.com/idm/api/ext/meta/v1.0", + "alias" : "RS-META", + "updated" : "2011-01-12T11:22:33-06:00", + "description" : "Allows associating arbritrary metadata with a user.", + "links" : [ + { + "rel" : "describedby", + "type" : "application/pdf", + "href" : "http://docs.rackspacecloud.com/idm/api/ext/idm-meta-20111201.pdf" + }, + { + "rel" : "describedby", + "type" : "application/vnd.sun.wadl+xml", + "href" : "http://docs.rackspacecloud.com/idm/api/ext/idm-cbs.wadl" + } + ] + } +} diff --git a/docs/guide/src/docbkx/samples/extension.xml b/docs/guide/src/docbkx/samples/extension.xml new file mode 100644 index 00000000..8d932f49 --- /dev/null +++ b/docs/guide/src/docbkx/samples/extension.xml @@ -0,0 +1,23 @@ + + + + + + Allows associating arbritrary metadata with a user. + + + + + + + + diff --git a/docs/guide/src/docbkx/samples/extensions.json b/docs/guide/src/docbkx/samples/extensions.json new file mode 100644 index 00000000..11064a90 --- /dev/null +++ b/docs/guide/src/docbkx/samples/extensions.json @@ -0,0 +1,44 @@ +{ + "extensions" : { + "values" : [ + { + "name" : "Reset Password Extension", + "namespace" : "http://docs.rackspacecloud.com/idm/api/ext/rpe/v1.0", + "alias" : "RS-RPE", + "updated" : "2011-01-22T13:25:27-06:00", + "description" : "Adds the capability to reset a user's password. The user is emailed when the password has been reset.", + "links" : [ + { + "rel" : "describedby", + "type" : "application/pdf", + "href" : "http://docs.rackspacecloud.com/idm/api/ext/idm-rpe-20111111.pdf" + }, + { + "rel" : "describedby", + "type" : "application/vnd.sun.wadl+xml", + "href" : "http://docs.rackspacecloud.com/idm/api/ext/idm-rpe.wadl" + } + ] + }, + { + "name" : "User Metadata Extension", + "namespace" : "http://docs.rackspacecloud.com/idm/api/ext/meta/v1.0", + "alias" : "RS-META", + "updated" : "2011-01-12T11:22:33-06:00", + "description" : "Allows associating arbritrary metadata with a user.", + "links" : [ + { + "rel" : "describedby", + "type" : "application/pdf", + "href" : "http://docs.rackspacecloud.com/idm/api/ext/idm-meta-20111201.pdf" + }, + { + "rel" : "describedby", + "type" : "application/vnd.sun.wadl+xml", + "href" : "http://docs.rackspacecloud.com/idm/api/ext/idm-meta.wadl" + } + ] + } + ] + } +} diff --git a/docs/guide/src/docbkx/samples/extensions.xml b/docs/guide/src/docbkx/samples/extensions.xml new file mode 100644 index 00000000..ddd7e173 --- /dev/null +++ b/docs/guide/src/docbkx/samples/extensions.xml @@ -0,0 +1,39 @@ + + + + + + + Adds the capability to reset a user's password. The user is + emailed when the password has been reset. + + + + + + + + Allows associating arbritrary metadata with a user. + + + + + + diff --git a/docs/guide/src/docbkx/samples/getuser-1.json b/docs/guide/src/docbkx/samples/getuser-1.json new file mode 100644 index 00000000..544d4600 --- /dev/null +++ b/docs/guide/src/docbkx/samples/getuser-1.json @@ -0,0 +1,29 @@ +{"user": + { + "groups": { + "values": [ + { + "tenantId" : "1234", + "id": "Admin" + }, + { + "tenantId" : "1234", + "id" : "DBUser" + }, + { + "id" : "Super" + } + ], + "links" : [ + { + "rel" : "next", + "href" : "http://idm.api.openstack.org/v1.0/1234/tenants/1234/users/jqsmith/groups?marker=Super" + } + ] + }, + "id": "jqsmith", + "tenantId": "1234", + "email": "john.smith@example.org", + "enabled": true + } +} diff --git a/docs/guide/src/docbkx/samples/getuser-1.xml b/docs/guide/src/docbkx/samples/getuser-1.xml new file mode 100644 index 00000000..2b513072 --- /dev/null +++ b/docs/guide/src/docbkx/samples/getuser-1.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/docs/guide/src/docbkx/samples/idm_fault.json b/docs/guide/src/docbkx/samples/idm_fault.json new file mode 100644 index 00000000..84e3908e --- /dev/null +++ b/docs/guide/src/docbkx/samples/idm_fault.json @@ -0,0 +1,7 @@ +{"idmFault": + { + "message": "Fault", + "details": "Error Details...", + "code": 500 + } +} \ No newline at end of file diff --git a/docs/guide/src/docbkx/samples/idm_fault.xml b/docs/guide/src/docbkx/samples/idm_fault.xml new file mode 100644 index 00000000..e0f695f2 --- /dev/null +++ b/docs/guide/src/docbkx/samples/idm_fault.xml @@ -0,0 +1,6 @@ + + + Fault +
Error Details...
+
diff --git a/docs/guide/src/docbkx/samples/item_not_found.json b/docs/guide/src/docbkx/samples/item_not_found.json new file mode 100644 index 00000000..dda88a59 --- /dev/null +++ b/docs/guide/src/docbkx/samples/item_not_found.json @@ -0,0 +1,7 @@ +{"itemNotFound": + { + "message": "Item not found.", + "details": "Error Details...", + "code": 404 + } +} \ No newline at end of file diff --git a/docs/guide/src/docbkx/samples/item_not_found.xml b/docs/guide/src/docbkx/samples/item_not_found.xml new file mode 100644 index 00000000..f967b76a --- /dev/null +++ b/docs/guide/src/docbkx/samples/item_not_found.xml @@ -0,0 +1,6 @@ + + + Item not found. +
Error Details...
+
diff --git a/docs/guide/src/docbkx/samples/samplerequestheader.json b/docs/guide/src/docbkx/samples/samplerequestheader.json new file mode 100644 index 00000000..a4647076 --- /dev/null +++ b/docs/guide/src/docbkx/samples/samplerequestheader.json @@ -0,0 +1,4 @@ +POST /v1.0/token HTTP/1.1 +Host: idm.api.rackspace.com +Content-Type: application/json +Accept: application/xml \ No newline at end of file diff --git a/docs/guide/src/docbkx/samples/sampleresponseheader.json b/docs/guide/src/docbkx/samples/sampleresponseheader.json new file mode 100644 index 00000000..0b08f684 --- /dev/null +++ b/docs/guide/src/docbkx/samples/sampleresponseheader.json @@ -0,0 +1,5 @@ +HTTP/1.1 200 OKAY +Date: Mon, 12 Nov 2010 15:55:01 GMT +Server: Apache +Content-Length: +Content-Type: application/xml; charset=UTF-8 \ No newline at end of file diff --git a/docs/guide/src/docbkx/samples/tenant.json b/docs/guide/src/docbkx/samples/tenant.json new file mode 100644 index 00000000..7ff7ce32 --- /dev/null +++ b/docs/guide/src/docbkx/samples/tenant.json @@ -0,0 +1,7 @@ +{"tenant": + { + "id": "1234", + "description": "A description ...", + "enabled": true + } +} diff --git a/docs/guide/src/docbkx/samples/tenant.xml b/docs/guide/src/docbkx/samples/tenant.xml new file mode 100644 index 00000000..ea6a63dc --- /dev/null +++ b/docs/guide/src/docbkx/samples/tenant.xml @@ -0,0 +1,5 @@ + + + A description... + diff --git a/docs/guide/src/docbkx/samples/tenantlock.json b/docs/guide/src/docbkx/samples/tenantlock.json new file mode 100644 index 00000000..584c21a4 --- /dev/null +++ b/docs/guide/src/docbkx/samples/tenantlock.json @@ -0,0 +1,5 @@ +{"tenant": + { + "description": "A NEW description..." + } +} \ No newline at end of file diff --git a/docs/guide/src/docbkx/samples/tenantlock.xml b/docs/guide/src/docbkx/samples/tenantlock.xml new file mode 100644 index 00000000..9d7081d4 --- /dev/null +++ b/docs/guide/src/docbkx/samples/tenantlock.xml @@ -0,0 +1,4 @@ + + + A NEW description... + diff --git a/docs/guide/src/docbkx/samples/tenants-1.json b/docs/guide/src/docbkx/samples/tenants-1.json new file mode 100644 index 00000000..6f45f1c8 --- /dev/null +++ b/docs/guide/src/docbkx/samples/tenants-1.json @@ -0,0 +1,16 @@ +{ +"tenants": { + "values" : [ + { + "id": "1234", + "description": "A description ...", + "enabled": true + } + ], + "links" : [ + { + "rel" : "next", + "href" : "http://idm.api.openstack.org/v1.0/1234/tenants?limit=1&marker=1234" + } + ] +} diff --git a/docs/guide/src/docbkx/samples/tenants-1.xml b/docs/guide/src/docbkx/samples/tenants-1.xml new file mode 100644 index 00000000..67101816 --- /dev/null +++ b/docs/guide/src/docbkx/samples/tenants-1.xml @@ -0,0 +1,10 @@ + + + + A description... + + diff --git a/docs/guide/src/docbkx/samples/tenants-2.json b/docs/guide/src/docbkx/samples/tenants-2.json new file mode 100644 index 00000000..7cb3ca6b --- /dev/null +++ b/docs/guide/src/docbkx/samples/tenants-2.json @@ -0,0 +1,20 @@ +{ +"tenants": { + "values" : [ + { + "id": "3645", + "description": "A description ...", + "enabled": true + } + ], + "links" : [ + { + "rel" : "next", + "href" : "http://idm.api.openstack.org/v1.0/1234/tenants?limit=1&marker=3645" + }, + { + "rel" : "previous", + "href" : "http://idm.api.openstack.org/v1.0/1234/tenants?limit=1" + } + ] +} diff --git a/docs/guide/src/docbkx/samples/tenants-2.xml b/docs/guide/src/docbkx/samples/tenants-2.xml new file mode 100644 index 00000000..7be4f537 --- /dev/null +++ b/docs/guide/src/docbkx/samples/tenants-2.xml @@ -0,0 +1,13 @@ + + + + A description... + + + diff --git a/docs/guide/src/docbkx/samples/tenants-3.json b/docs/guide/src/docbkx/samples/tenants-3.json new file mode 100644 index 00000000..febcf225 --- /dev/null +++ b/docs/guide/src/docbkx/samples/tenants-3.json @@ -0,0 +1,16 @@ +{ +"tenants": { + "values" : [ + { + "id": "9999", + "description": "A description ...", + "enabled": true + } + ], + "links" : [ + { + "rel" : "previous", + "href" : "http://idm.api.openstack.org/v1.0/1234/tenants?limit=1&marker=1234" + } + ] +} diff --git a/docs/guide/src/docbkx/samples/tenants-3.xml b/docs/guide/src/docbkx/samples/tenants-3.xml new file mode 100644 index 00000000..5e5b49fa --- /dev/null +++ b/docs/guide/src/docbkx/samples/tenants-3.xml @@ -0,0 +1,10 @@ + + + + A description... + + + diff --git a/docs/guide/src/docbkx/samples/tenants.json b/docs/guide/src/docbkx/samples/tenants.json new file mode 100644 index 00000000..7fb848f7 --- /dev/null +++ b/docs/guide/src/docbkx/samples/tenants.json @@ -0,0 +1,15 @@ +{"tenants": { + "values" : [ + { + "id": "1234", + "description": "A description ...", + "enabled": true + }, + { + "id": "3456", + "description": "A description ...", + "enabled": true + } + ] +} +} diff --git a/docs/guide/src/docbkx/samples/tenants.xml b/docs/guide/src/docbkx/samples/tenants.xml new file mode 100644 index 00000000..9b503ce2 --- /dev/null +++ b/docs/guide/src/docbkx/samples/tenants.xml @@ -0,0 +1,9 @@ + + + + A description... + + + A description... + + diff --git a/docs/guide/src/docbkx/samples/updatedtenant.json b/docs/guide/src/docbkx/samples/updatedtenant.json new file mode 100644 index 00000000..05df6a3a --- /dev/null +++ b/docs/guide/src/docbkx/samples/updatedtenant.json @@ -0,0 +1,7 @@ +{"tenant": + { + "id": "1234", + "description": "A NEW description...", + "enabled": true + } +} diff --git a/docs/guide/src/docbkx/samples/updatedtenant.xml b/docs/guide/src/docbkx/samples/updatedtenant.xml new file mode 100644 index 00000000..8bcce9bf --- /dev/null +++ b/docs/guide/src/docbkx/samples/updatedtenant.xml @@ -0,0 +1,5 @@ + + + A NEW description... + diff --git a/docs/guide/src/docbkx/samples/validatetoken.json b/docs/guide/src/docbkx/samples/validatetoken.json new file mode 100644 index 00000000..c4721873 --- /dev/null +++ b/docs/guide/src/docbkx/samples/validatetoken.json @@ -0,0 +1,19 @@ +{ + "auth" : { + "token": { + "id": "ab48a9efdfedb23ty3494", + "expires": "2010-11-01T03:32:15-05:00" + }, + "user" : { + "groups": { + "group": [ + { + "tenantId" : "1234", + "name": "Admin" + } + ]}, + "username": "jqsmith", + "tenantId": "1234", + } + } +} diff --git a/docs/guide/src/docbkx/samples/validatetoken.xml b/docs/guide/src/docbkx/samples/validatetoken.xml new file mode 100644 index 00000000..b091c7af --- /dev/null +++ b/docs/guide/src/docbkx/samples/validatetoken.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/docs/guide/src/docbkx/samples/version-atom.xml b/docs/guide/src/docbkx/samples/version-atom.xml new file mode 100644 index 00000000..8cf3dc88 --- /dev/null +++ b/docs/guide/src/docbkx/samples/version-atom.xml @@ -0,0 +1,19 @@ + + + About This Version + 2011-01-21T11:33:21-06:00 + http://idm.api.openstack.org/v1.0/ + Rackspacehttp://www.rackspace.com/ + + + http://idm.api.openstack.org/v1.0/ + Version v1.0 + 2011-01-21T11:33:21-06:00 + + + + Version v1.0 CURRENT (2011-01-21T11:33:21-06:00) + + diff --git a/docs/guide/src/docbkx/samples/version.json b/docs/guide/src/docbkx/samples/version.json new file mode 100644 index 00000000..74bae5bd --- /dev/null +++ b/docs/guide/src/docbkx/samples/version.json @@ -0,0 +1,33 @@ +{ + "version" : { + "id" : "v1.0", + "status" : "CURRENT", + "updated" : "2011-01-21T11:33:21-06:00", + "links": [ + { + "rel" : "self", + "href" : "http://idm.api.openstack.org/v1.0/" + }, + { + "rel" : "describedby", + "type" : "application/pdf", + "href" : "http://docs.rackspacecloud.com/idm/api/v1.0/idm-devguide-20110125.pdf" + }, + { + "rel" : "describedby", + "type" : "application/vnd.sun.wadl+xml", + "href" : "http://docs.rackspacecloud.com/idm/api/v1.0/application.wadl" + } + ], + "media-types": [ + { + "base" : "application/xml", + "type" : "application/vnd.openstack.idm-v1.0+xml" + }, + { + "base" : "application/json", + "type" : "application/vnd.openstack.idm-v1.0+json" + } + ] + } +} diff --git a/docs/guide/src/docbkx/samples/version.xml b/docs/guide/src/docbkx/samples/version.xml new file mode 100644 index 00000000..4f499322 --- /dev/null +++ b/docs/guide/src/docbkx/samples/version.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/docs/guide/src/docbkx/samples/versions-atom.xml b/docs/guide/src/docbkx/samples/versions-atom.xml new file mode 100644 index 00000000..e75cf416 --- /dev/null +++ b/docs/guide/src/docbkx/samples/versions-atom.xml @@ -0,0 +1,22 @@ + + + Available API Versions + 2010-12-12T18:30:02.25Z + http://idm.api.openstack.org/ + Rackspacehttp://www.rackspace.com/ + + + http://idm.api.openstack.org/v1.1/ + Version v1.1 + 2010-12-12T18:30:02.25Z + + Version v1.1 CURRENT (2010-12-12T18:30:02.25Z) + + + http://idm.api.openstack.org/v1.0/ + Version v1.0 + 2009-10-09T11:30:00Z + + Version v1.0 DEPRECATED (2009-10-09T11:30:00Z) + + diff --git a/docs/guide/src/docbkx/samples/versions.json b/docs/guide/src/docbkx/samples/versions.json new file mode 100644 index 00000000..330a26e9 --- /dev/null +++ b/docs/guide/src/docbkx/samples/versions.json @@ -0,0 +1,28 @@ +{ + "versions" : { + "values" : [ + { + "id" : "v1.0", + "status" : "DEPRECATED", + "updated" : "2009-10-09T11:30:00Z", + "links": [ + { + "rel" : "self", + "href" : "http://idm.api.openstack.org/v1.0/" + } + ] + }, + { + "id" : "v1.1", + "status" : "CURRENT", + "updated" : "2010-12-12T18:30:02.25Z", + "links": [ + { + "rel" : "self", + "href" : "http://idm.api.openstack.org/v1.1/" + } + ] + } + ] + } +} diff --git a/docs/guide/src/docbkx/samples/versions.xml b/docs/guide/src/docbkx/samples/versions.xml new file mode 100644 index 00000000..7c3b1535 --- /dev/null +++ b/docs/guide/src/docbkx/samples/versions.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/docs/guide/src/docbkx/xsd/api-common.xsd b/docs/guide/src/docbkx/xsd/api-common.xsd new file mode 100644 index 00000000..5e8ebeda --- /dev/null +++ b/docs/guide/src/docbkx/xsd/api-common.xsd @@ -0,0 +1,56 @@ + + + + + + + + + + Open Stack Common API Schema Types 1.0 + + + + + +

+ This is the main index XML Schema document + for Common API Schema Types Version 1.0. +

+
+
+ + + +

+ Types related to extensions. +

+
+
+
+ + + +

+ Types related to API version details. +

+
+
+
+
diff --git a/docs/guide/src/docbkx/xsd/api.xsd b/docs/guide/src/docbkx/xsd/api.xsd new file mode 100644 index 00000000..eaa11c17 --- /dev/null +++ b/docs/guide/src/docbkx/xsd/api.xsd @@ -0,0 +1,14 @@ + + + + + + + diff --git a/docs/guide/src/docbkx/xsd/atom/atom.xsd b/docs/guide/src/docbkx/xsd/atom/atom.xsd new file mode 100644 index 00000000..a619efaa --- /dev/null +++ b/docs/guide/src/docbkx/xsd/atom/atom.xsd @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + See section 3.4 of the ATOM RFC RFC4287 + + + + + + + TODO + + + + + + + + TODO + + + + + + + + TODO + + + + + + + + TODO + + + + + + + + TODO + + + + + + + + diff --git a/docs/guide/src/docbkx/xsd/atom/xml.xsd b/docs/guide/src/docbkx/xsd/atom/xml.xsd new file mode 100644 index 00000000..aea7d0db --- /dev/null +++ b/docs/guide/src/docbkx/xsd/atom/xml.xsd @@ -0,0 +1,287 @@ + + + + + + +
+

About the XML namespace

+ +
+

+ This schema document describes the XML namespace, in a form + suitable for import by other schema documents. +

+

+ See + http://www.w3.org/XML/1998/namespace.html and + + http://www.w3.org/TR/REC-xml for information + about this namespace. +

+

+ Note that local names in this namespace are intended to be + defined only by the World Wide Web Consortium or its subgroups. + The names currently defined in this namespace are listed below. + They should not be used with conflicting semantics by any Working + Group, specification, or document instance. +

+

+ See further below in this document for more information about how to refer to this schema document from your own + XSD schema documents and about the + namespace-versioning policy governing this schema document. +

+
+
+
+
+ + + + +
+ +

lang (as an attribute name)

+

+ denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification.

+ +
+
+

Notes

+

+ Attempting to install the relevant ISO 2- and 3-letter + codes as the enumerated possible values is probably never + going to be a realistic possibility. +

+

+ See BCP 47 at + http://www.rfc-editor.org/rfc/bcp/bcp47.txt + and the IANA language subtag registry at + + http://www.iana.org/assignments/language-subtag-registry + for further information. +

+

+ The union allows for the 'un-declaration' of xml:lang with + the empty string. +

+
+
+
+ + + + + + + + + +
+ + + + +
+ +

space (as an attribute name)

+

+ denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification.

+ +
+
+
+ + + + + + +
+ + + +
+ +

base (as an attribute name)

+

+ denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification.

+ +

+ See http://www.w3.org/TR/xmlbase/ + for information about this attribute. +

+
+
+
+
+ + + + +
+ +

id (as an attribute name)

+

+ denotes an attribute whose value + should be interpreted as if declared to be of type ID. + This name is reserved by virtue of its definition in the + xml:id specification.

+ +

+ See http://www.w3.org/TR/xml-id/ + for information about this attribute. +

+
+
+
+
+ + + + + + + + + + +
+ +

Father (in any context at all)

+ +
+

+ denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: +

+
+

+ In appreciation for his vision, leadership and + dedication the W3C XML Plenary on this 10th day of + February, 2000, reserves for Jon Bosak in perpetuity + the XML name "xml:Father". +

+
+
+
+
+
+ + + +
+

About this schema document

+ +
+

+ This schema defines attributes and an attribute group suitable + for use by schemas wishing to allow xml:base, + xml:lang, xml:space or + xml:id attributes on elements they define. +

+

+ To enable this, such a schema must import this schema for + the XML namespace, e.g. as follows: +

+
+          <schema . . .>
+           . . .
+           <import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+     
+

+ or +

+
+           <import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
+     
+

+ Subsequently, qualified reference to any of the attributes or the + group defined below will have the desired effect, e.g. +

+
+          <type . . .>
+           . . .
+           <attributeGroup ref="xml:specialAttrs"/>
+     
+

+ will define a type which will schema-validate an instance element + with any of those attributes. +

+
+
+
+
+ + + +
+

Versioning policy for this schema document

+
+

+ In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + + http://www.w3.org/2009/01/xml.xsd. +

+

+ At the date of issue it can also be found at + + http://www.w3.org/2001/xml.xsd. +

+

+ The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML + Schema itself, or with the XML namespace itself. In other words, + if the XML Schema or XML namespaces change, the version of this + document at + http://www.w3.org/2001/xml.xsd + + will change accordingly; the version at + + http://www.w3.org/2009/01/xml.xsd + + will not change. +

+

+ Previous dated (and unchanging) versions of this schema + document are at: +

+ +
+
+
+
+ +
+ diff --git a/docs/guide/src/docbkx/xsd/extensions.xsd b/docs/guide/src/docbkx/xsd/extensions.xsd new file mode 100644 index 00000000..a942f0a1 --- /dev/null +++ b/docs/guide/src/docbkx/xsd/extensions.xsd @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ There should be at least one atom link + with a describedby relation. +

+
+
+
+
+ + + + + + + +
diff --git a/docs/guide/src/docbkx/xsd/fault.xsd b/docs/guide/src/docbkx/xsd/fault.xsd new file mode 100644 index 00000000..8701b9f7 --- /dev/null +++ b/docs/guide/src/docbkx/xsd/fault.xsd @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + +

+ A human readable message that is appropriate for display + to the end user. +

+
+
+
+ + + +

+ The optional <details> element may contain useful + information for tracking down errors (e.g a stack + trace). This information may or may not be appropriate + for display to an end user. +

+
+
+
+ +
+ + + +

+ The HTTP status code associated with the current fault. +

+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ An optional dateTime denoting when an operation should + be retried. +

+
+
+
+
+
+
+ +
diff --git a/docs/guide/src/docbkx/xsd/tenant.xsd b/docs/guide/src/docbkx/xsd/tenant.xsd new file mode 100644 index 00000000..2e4854fb --- /dev/null +++ b/docs/guide/src/docbkx/xsd/tenant.xsd @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/guide/src/docbkx/xsd/token.xsd b/docs/guide/src/docbkx/xsd/token.xsd new file mode 100644 index 00000000..80bcb029 --- /dev/null +++ b/docs/guide/src/docbkx/xsd/token.xsd @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/guide/src/docbkx/xsd/version.xsd b/docs/guide/src/docbkx/xsd/version.xsd new file mode 100644 index 00000000..6b2403bd --- /dev/null +++ b/docs/guide/src/docbkx/xsd/version.xsd @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + The VersionStatus type describes a service's operational status. + + + + + + + + + + + + + + + + + A version choice list outlines a collection of service version choices. + + + + + + + + + + + + + + In version lists, every single version must + contain at least one self link. + + + + + + + + + + + + + + When used as a root element, a version choice + must contain at least one describedby link. + + + + + + + + + + + + + A version choice contains relevant information about an available service + that a user can then use to target a specific version of the service. Note + that both the descriptive media types and the atom link references are + not manditory and are offered as message enrichment elements rather + than message requirements. + + + + + + + + + + + + + + + The ID of a version choice represents the service version's unique + identifier. This ID is guaranteed to be unique only among the + service version choices outlined in the VersionChoiceList. + + + + + + + + + + A version choice's status describes the current operational state of + the given service version. The operational status is captured in a + simple type enumeration called VersionStatus. + + + + + + + + + + A version choice's updated attribute describes + the time when the version was updated. The + time should be updated anytime + anything in the + version has changed: documentation, + extensions, bug fixes. + + + + + + + + + + + + A MediaTypeList outlines a collection of valid media types for a given + service version. + + + + + + + + + + + + + + + + A MediaType describes what content types the service version understands. + + + + + + + + + + + The base of a given media type describes the simple MIME type + that then a more complicated media type can be derived from. These + types are basic and provide no namespace or version specific + data are are only provided as a convenience. Because of this the + base attribute is declared as optional. + + + + + + + + + + The type attribute of a MediaType describes the MIME specific + identifier of the media type in question. This identifier should include + a vendor namespace ( + See RFC 2048) + as well as a version suffix. + + + + + + + diff --git a/echo/echo/__init__.py b/echo/echo/__init__.py new file mode 100644 index 00000000..52bcde04 --- /dev/null +++ b/echo/echo/__init__.py @@ -0,0 +1 @@ +from echo import app_factory diff --git a/echo/echo/echo.ini b/echo/echo/echo.ini new file mode 100644 index 00000000..b81174d0 --- /dev/null +++ b/echo/echo/echo.ini @@ -0,0 +1,36 @@ +[DEFAULT] +;delegated means we still allow unauthenticated requests through so the +;service can make the final decision on authentication +delay_auth_decision = 0 + +;where to find the OpenStack service (if not in local WSGI chain) +service_protocol = http +service_host = 127.0.0.1 +service_port = 8090 +;used to verify this component with the OpenStack service (or PAPIAuth) +service_pass = dTpw + + +[app:echo] +paste.app_factory = echo:app_factory + +[pipeline:main] +pipeline = + tokenauth + echo + +[filter:tokenauth] +paste.filter_factory = keystone:tokenauth_factory +;where to find the token auth service +auth_host = 127.0.0.1 +auth_port = 8080 +auth_protocol = http +;how to authenticate to the auth service for priviledged operations +;like validate token +admin_token = 999888777666 + +[filter:basicauth] +paste.filter_factory = keystone:basicauth_factory + +[filter:openidauth] +paste.filter_factory = keystone:openidauth_factory diff --git a/echo/echo/echo.py b/echo/echo/echo.py new file mode 100644 index 00000000..ee950b37 --- /dev/null +++ b/echo/echo/echo.py @@ -0,0 +1,121 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2010-2011 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 os +import sys + +import eventlet +from eventlet import wsgi +from lxml import etree +from paste.deploy import loadapp + +# If ../echo/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'echo', '__init__.py')): + # also use the local keystone + KEYSTONE_TOPDIR = os.path.normpath(os.path.join(POSSIBLE_TOPDIR, + os.pardir)) + if os.path.exists(os.path.join(KEYSTONE_TOPDIR, + 'keystone', + '__init__.py')): + sys.path.insert(0, KEYSTONE_TOPDIR) + sys.path.insert(0, POSSIBLE_TOPDIR) + + +""" +Echo: a dummy service for OpenStack auth testing. It returns request info. +""" + + +class EchoApp(object): + def __init__(self, environ, start_response): + self.envr = environ + self.start = start_response + self.dom = self.toDOM(environ) + echo_xsl = os.path.join(os.path.abspath(\ + os.path.dirname(__file__)), "xsl/echo.xsl") + self.transform = etree.XSLT(etree.parse(echo_xsl)) + + def __iter__(self): + if 'HTTP_X_AUTHORIZATION' not in self.envr: + return HTTPUnauthorized(self.envr, start_response) + + print ' Received:' + if 'HTTP_X_IDENTITY_STATUS' in self.envr: print ' Auth Status:', self.envr['HTTP_X_IDENTITY_STATUS'] + if 'HTTP_X_AUTHORIZATION' in self.envr: print ' Identity :', self.envr['HTTP_X_AUTHORIZATION'] + if 'HTTP_X_TENANT' in self.envr: print ' Tenant :', self.envr['HTTP_X_TENANT'] + if 'HTTP_X_GROUP' in self.envr: print ' Group :', self.envr['HTTP_X_GROUP'] + + accept = self.envr.get("HTTP_ACCEPT", "application/json") + if accept == "application/xml": + return self.toXML() + else: + return self.toJSON() + + def toJSON(self): + self.start('200 OK', [('Content-Type', 'application/json')]) + yield str(self.transform(self.dom)) + + def toXML(self): + self.start('200 OK', [('Content-Type', 'application/xml')]) + yield etree.tostring(self.dom) + + def toDOM(self, environ): + echo = etree.Element("{http://docs.openstack.org/echo/api/v1.0}echo", + method=environ["REQUEST_METHOD"], + pathInfo=environ["PATH_INFO"], + queryString=environ.get('QUERY_STRING', ""), + caller_identity=self.envr['HTTP_X_AUTHORIZATION']) + content = etree.Element( + "{http://docs.openstack.org/echo/api/v1.0}content") + content.set("type", environ["CONTENT_TYPE"]) + content.text = "" + inReq = environ["wsgi.input"] + for line in inReq: + content.text = content.text + line + echo.append(content) + return echo + + +def app_factory(global_conf, **local_conf): + return EchoApp + +if __name__ == "__main__": + remote_auth = False + if len(sys.argv) > 1: + remote_auth = sys.argv[1] == '--remote' + + if remote_auth: + # running auth remotely + print "Running for use with remote auth" + + app = loadapp("config:" + \ + os.path.join(os.path.abspath(os.path.dirname(__file__)), + "echo_remote.ini"), global_conf={"log_name": "echo.log"}) + + wsgi.server(eventlet.listen(('', 8100)), app) + + else: + print "Running all components locally." + print "Use --remote option to run with remote auth proxy" + app = loadapp("config:" + \ + os.path.join(os.path.abspath(os.path.dirname(__file__)), + "echo.ini"), global_conf={"log_name": "echo.log"}) + + wsgi.server(eventlet.listen(('', 8090)), app) diff --git a/echo/echo/echo.wadl b/echo/echo/echo.wadl new file mode 100644 index 00000000..b9572999 --- /dev/null +++ b/echo/echo/echo.wadl @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/echo/echo/echo_remote.ini b/echo/echo/echo_remote.ini new file mode 100644 index 00000000..c27b2365 --- /dev/null +++ b/echo/echo/echo_remote.ini @@ -0,0 +1,19 @@ +[DEFAULT] + +[app:echo] +paste.app_factory = echo:app_factory + +[pipeline:main] +pipeline = + remoteauth + echo + +[filter:remoteauth] +paste.filter_factory = keystone:remoteauth_factory +;password which will tell us call is coming from a trusted auth proxy +; (otherwise we redirect call) +remote_auth_pass = dTpw +;where to redirect untrusted calls to +auth_location = http://127.0.0.1:8080/ + + diff --git a/echo/echo/samples/echo.json b/echo/echo/samples/echo.json new file mode 100644 index 00000000..05b60ef4 --- /dev/null +++ b/echo/echo/samples/echo.json @@ -0,0 +1,11 @@ +{ + "echo" : { + "method" : "GET", + "pathInfo" : "/bla/bla", + "queryString" : "hello", + "content" : { + "type" : "application/xml", + "value" : " This is some content. " + } + } +} diff --git a/echo/echo/samples/echo.xml b/echo/echo/samples/echo.xml new file mode 100644 index 00000000..8cf4b984 --- /dev/null +++ b/echo/echo/samples/echo.xml @@ -0,0 +1,8 @@ + + + + + This is some content. + + diff --git a/echo/echo/xsd/echo.xsd b/echo/echo/xsd/echo.xsd new file mode 100644 index 00000000..3d5c2a51 --- /dev/null +++ b/echo/echo/xsd/echo.xsd @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/echo/echo/xsl/echo.xsl b/echo/echo/xsl/echo.xsl new file mode 100644 index 00000000..dbe8b5dd --- /dev/null +++ b/echo/echo/xsl/echo.xsl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + diff --git a/echo/echo_client.py b/echo/echo_client.py new file mode 100644 index 00000000..677e8ab9 --- /dev/null +++ b/echo/echo_client.py @@ -0,0 +1,72 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2010-2011 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. +""" +Implement a client for Echo service using Identity service +""" + +import httplib +import json + + +def get_auth_token(username, password, tenant): + headers = {"Content-type": "application/json", "Accept": "text/json"} + params = {"passwordCredentials": {"username": username, + "password": password, + "tenantId": "1"}} + conn = httplib.HTTPConnection("localhost:8080") + conn.request("POST", "/v1.0/token", json.dumps(params), headers=headers) + response = conn.getresponse() + data = response.read() + ret = data + return ret + + +def call_service(token): + headers = {"X-Auth-Token": token, + "Content-type": "application/json", + "Accept": "text/json"} + params = '{"ping": "abcdefg"}' + conn = httplib.HTTPConnection("localhost:8090") + conn.request("POST", "/", params, headers=headers) + response = conn.getresponse() + data = response.read() + ret = data + return ret + +if __name__ == '__main__': + # Call the keystone service to get a token + # NOTE: assumes the test_setup.sql script has loaded this user + print "\033[91mTrying with valid test credentials...\033[0m" + auth = get_auth_token("joeuser", "secrete", "1") + obj = json.loads(auth) + token = obj["auth"]["token"]["id"] + print "Token obtained:", token + + # Use that token to call an OpenStack service (echo) + data = call_service(token) + print "Response received:", data + print + + # Use bad token to call an OpenStack service (echo) + print "\033[91mTrying with bad token...\033[0m" + data = call_service("xxxx_invalid_token_xxxx") + print "Response received:", data + print + + #Supply bad credentials + print "\033[91mTrying with bad credentials...\033[0m" + auth = get_auth_token("joeuser", "wrongpass", "1") + print "Response:", auth diff --git a/echo/setup.py b/echo/setup.py new file mode 100644 index 00000000..045df550 --- /dev/null +++ b/echo/setup.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +# Copyright (c) 2010-2011 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. + +from setuptools import setup, find_packages + +version = '1.0' + +setup( + name='echo', + version=version, + description="", + license='Apache License (2.0)', + classifiers=["Programming Language :: Python"], + keywords='', + author='OpenStack, LLC.', + include_package_data=True, + packages=find_packages(exclude=['test', 'bin']), + zip_safe=False, + install_requires=['setuptools', 'keystone'], + entry_points={ + 'paste.app_factory': ['main=echo:app_factory'], + 'paste.filter_factory': [ + 'papiauth=keystone:papiauth_factory', + ], + }, + ) diff --git a/keystone/__init__.py b/keystone/__init__.py new file mode 100644 index 00000000..aec9d44f --- /dev/null +++ b/keystone/__init__.py @@ -0,0 +1,15 @@ +#TOKEN AUTH +from auth_protocols.auth_token \ + import filter_factory as tokenauth_factory + +#BASIC AUTH +from auth_protocols.auth_basic \ + import filter_factory as basicauth_factory + +#OPENID AUTH +from auth_protocols.auth_openid \ + import filter_factory as openidauth_factory + +#Remote Auth handler +from middleware.remoteauth \ + import filter_factory as remoteauth_factory diff --git a/keystone/auth_protocols/__init__.py b/keystone/auth_protocols/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone/auth_protocols/auth_basic.py b/keystone/auth_protocols/auth_basic.py new file mode 100644 index 00000000..2bc967ff --- /dev/null +++ b/keystone/auth_protocols/auth_basic.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (c) 2010-2011 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. +# Not Yet PEP8 standardized + + +""" +BASIC AUTH MIDDLEWARE - STUB + +This WSGI component should perform multiple jobs: +- validate incoming basic claims +- perform all basic auth interactions with clients +- collect and forward identity information from the authentication process + such as user name, groups, etc... + +This is an Auth component as per: http://wiki.openstack.org/openstack-authn + +""" + + +PROTOCOL_NAME = "Basic Authentication" + + +class AuthProtocol(object): + """Auth Middleware that handles authenticating client calls""" + + def __init__(self, app, conf): + print "Starting the %s component" % PROTOCOL_NAME + + self.conf = conf + self.app = app + #if app is set, then we are in a WSGI pipeline and requests get passed + # on to app. If it is not set, this component should forward requests + + # where to find the OpenStack service (if not in local WSGI chain) + # these settings are only used if this component is acting as a proxy + # and the OpenSTack service is running remotely + self.service_protocol = conf.get('service_protocol', 'https') + self.service_host = conf.get('service_host') + self.service_port = int(conf.get('service_port')) + self.service_url = '%s://%s:%s' % (self.service_protocol, + self.service_host, + self.service_port) + # used to verify this component with the OpenStack service or PAPIAuth + self.service_pass = conf.get('service_pass') + + # delay_auth_decision means we still allow unauthenticated requests + # through and we let the downstream service make the final decision + self.delay_auth_decision = int(conf.get('delay_auth_decision', 0)) + + def __call__(self, env, start_response): + def custom_start_response(status, headers): + if self.delay_auth_decision: + headers.append(('WWW-Authenticate', "Basic realm='API Realm'")) + return start_response(status, headers) + + #TODO(Ziad): PERFORM BASIC AUTH + + #Auth processed, headers added now decide how to pass on the call + if self.app: + # Pass to downstream WSGI component + env['HTTP_AUTHORIZATION'] = "Basic %s" % self.service_pass + return self.app(env, custom_start_response) + + proxy_headers['AUTHORIZATION'] = "Basic %s" % self.service_pass + # We are forwarding to a remote service (no downstream WSGI app) + req = Request(proxy_headers) + parsed = urlparse(req.url) + conn = http_connect(self.service_host, self.service_port, \ + req.method, parsed.path, \ + proxy_headers,\ + ssl=(self.service_protocol == 'https')) + resp = conn.getresponse() + data = resp.read() + #TODO: use a more sophisticated proxy + # we are rewriting the headers now + return Response(status=resp.status, body=data)(env, start_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) + +if __name__ == "__main__": + app = loadapp("config:" + \ + os.path.join(os.path.abspath(os.path.dirname(__file__)), + "auth_basic.ini"), + global_conf={"log_name": "auth_basic.log"}) + wsgi.server(eventlet.listen(('', 8090)), app) diff --git a/keystone/auth_protocols/auth_openid.py b/keystone/auth_protocols/auth_openid.py new file mode 100644 index 00000000..ac9121f7 --- /dev/null +++ b/keystone/auth_protocols/auth_openid.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (c) 2010-2011 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. +# Not Yet PEP8 standardized + + +""" +OPENID AUTH MIDDLEWARE - STUB + +This WSGI component should perform multiple jobs: +- validate incoming openid claims +- perform all openid interactions with clients +- collect and forward identity information from the openid authentication + such as user name, groups, etc... + +This is an Auth component as per: http://wiki.openstack.org/openstack-authn +""" + + +PROTOCOL_NAME = "OpenID Authentication" + + +class AuthProtocol(object): + """Auth Middleware that handles authenticating client calls""" + + def __init__(self, app, conf): + print "Starting the %s component" % PROTOCOL_NAME + + self.conf = conf + self.app = app + #if app is set, then we are in a WSGI pipeline and requests get passed + # on to app. If it is not set, this component should forward requests + + # where to find the OpenStack service (if not in local WSGI chain) + # these settings are only used if this component is acting as a proxy + # and the OpenSTack service is running remotely + self.service_protocol = conf.get('service_protocol', 'http') + self.service_host = conf.get('service_host', '127.0.0.1') + self.service_port = int(conf.get('service_port', 8090)) + self.service_url = '%s://%s:%s' % (self.service_protocol, + self.service_host, + self.service_port) + # used to verify this component with the OpenStack service or PAPIAuth + self.service_pass = conf.get('service_pass', 'dTpw') + + # delay_auth_decision means we still allow unauthenticated requests + # through and we let the downstream service make the final decision + self.delay_auth_decision = int(conf.get('delay_auth_decision', 0)) + + def __call__(self, env, start_response): + def custom_start_response(status, headers): + if self.delay_auth_decision: + headers.append(('WWW-Authenticate', "Basic realm='API Realm'")) + return start_response(status, headers) + + #TODO(Rasib): PERFORM OPENID AUTH + + #Auth processed, headers added now decide how to pass on the call + if self.app: + # Pass to downstream WSGI component + env['HTTP_AUTHORIZATION'] = "Basic %s" % self.service_pass + return self.app(env, custom_start_response) + + proxy_headers['AUTHORIZATION'] = "Basic %s" % self.service_pass + # We are forwarding to a remote service (no downstream WSGI app) + req = Request(proxy_headers) + parsed = urlparse(req.url) + conn = http_connect(self.service_host, self.service_port, \ + req.method, parsed.path, \ + proxy_headers,\ + ssl=(self.service_protocol == 'https')) + resp = conn.getresponse() + data = resp.read() + #TODO: use a more sophisticated proxy + # we are rewriting the headers now + return Response(status=resp.status, body=data)(env, start_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) + +if __name__ == "__main__": + app = loadapp("config:" + \ + os.path.join(os.path.abspath(os.path.dirname(__file__)), + "auth_openid.ini"), + global_conf={"log_name": "auth_openid.log"}) + wsgi.server(eventlet.listen(('', 8090)), app) diff --git a/keystone/auth_protocols/auth_token.ini b/keystone/auth_protocols/auth_token.ini new file mode 100644 index 00000000..3154fc69 --- /dev/null +++ b/keystone/auth_protocols/auth_token.ini @@ -0,0 +1,18 @@ +[DEFAULT] + +[app:main] +paste.app_factory = auth_token:app_factory + +auth_protocol = http +auth_host = 127.0.0.1 +auth_port = 8080 +admin_token = 999888777666 + +delay_auth_decision = 0 + +service_protocol = http +service_host = 127.0.0.1 +service_port = 8100 +service_pass = dTpw + + diff --git a/keystone/auth_protocols/auth_token.py b/keystone/auth_protocols/auth_token.py new file mode 100644 index 00000000..cee8e84f --- /dev/null +++ b/keystone/auth_protocols/auth_token.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (c) 2010-2011 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. +# Not Yet PEP8 standardized + + +""" +TOKEN-BASED AUTH MIDDLEWARE + +This WSGI component performs multiple jobs: +- it verifies that incoming client requests have valid tokens by verifying + tokens with the auth service. +- it will reject 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) +- it will collect and forward identity information from a valid token + such as user name, groups, etc... + +Refer to: http://wiki.openstack.org/openstack-authn + + +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 cloud files +> Used for communication between components +www-authenticate : only used if this component is being used remotely +HTTP_AUTHORIZATION : basic auth password used to validate the connection + +> What we add to the request for use by the OpenStack service +HTTP_X_AUTHORIZATION: the client identity being passed in + +""" + +import eventlet +from eventlet import wsgi +import json +import os +from paste.deploy import loadapp +from urlparse import urlparse +from webob.exc import HTTPUnauthorized +from webob.exc import Request, Response +import httplib + +from keystone.common.bufferedhttp import http_connect_raw as http_connect + +PROTOCOL_NAME = "Token Authentication" + + +def _decorate_request_headers(header, value, proxy_headers, env): + proxy_headers[header] = value + env["HTTP_%s" % header] = value + + +class AuthProtocol(object): + """Auth Middleware that handles authenticating client calls""" + + def _init_protocol_common(self, app, conf): + """ Common initialization code""" + print "Starting the %s component" % PROTOCOL_NAME + + self.conf = conf + self.app = app + #if app is set, then we are in a WSGI pipeline and requests get passed + # on to app. If it is not set, this component should forward requests + + # where to find the OpenStack service (if not in local WSGI chain) + # these settings are only used if this component is acting as a proxy + # and the OpenSTack service is running remotely + self.service_protocol = conf.get('service_protocol', 'https') + self.service_host = conf.get('service_host') + self.service_port = int(conf.get('service_port')) + self.service_url = '%s://%s:%s' % (self.service_protocol, + self.service_host, + self.service_port) + # used to verify this component with the OpenStack service or PAPIAuth + self.service_pass = conf.get('service_pass') + + # delay_auth_decision means we still allow unauthenticated requests + # through and we let the downstream service make the final decision + self.delay_auth_decision = int(conf.get('delay_auth_decision', 0)) + + def _init_protocol(self, app, conf): + """ Protocol specific initialization """ + + # where to find the auth service (we use this to validate tokens) + self.auth_host = conf.get('auth_host') + self.auth_port = int(conf.get('auth_port')) + self.auth_protocol = conf.get('auth_protocol', 'https') + self.auth_location = "%s://%s:%s" % (self.auth_protocol, self.auth_host, + self.auth_port) + + # Credentials used to verify this component with the Auth service since + # validating tokens is a priviledged call + self.admin_token = conf.get('admin_token') + + def __init__(self, app, conf): + """ Common initialization code """ + + #TODO: maybe we rafactor this into a superclass + self._init_protocol_common(app, conf) # Applies to all protocols + self._init_protocol(app, conf) # Specific to this protocol + + def get_admin_auth_token(self, username, password, tenant): + """ + This function gets an admin auth token to be used by this service to + validate a user's token. Validate_token is a priviledged call so + it needs to be authenticated by a service that is calling it + """ + headers = {"Content-type": "application/json", "Accept": "text/json"} + params = {"passwordCredentials": {"username": username, + "password": password, + "tenantId": "1"}} + conn = httplib.HTTPConnection("%s:%s" \ + % (self.auth_host, self.auth_port)) + conn.request("POST", "/v1.0/token", json.dumps(params), \ + headers=headers) + response = conn.getresponse() + data = response.read() + ret = data + return ret + + def __call__(self, env, start_response): + def custom_start_response(status, headers): + if self.delay_auth_decision: + headers.append(('WWW-Authenticate', "Basic realm='API Realm'")) + return start_response(status, headers) + + #Prep headers to proxy request to remote service + proxy_headers = env.copy() + user = '' + + #Look for token in request + token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) + if not token: + #No token was provided + if self.delay_auth_decision: + _decorate_request_headers("X_IDENTITY_STATUS", "Invalid", + proxy_headers, env) + else: + # Redirect client to auth server + return HTTPUseProxy(location=self.auth_location)(env, + start_response) + else: + # this request is claiming it has a valid token, let's check + # with the auth service + # Step 1: We need to auth with the keystone service, so get an + # admin token + #TODO: Need to properly implement this, where to store creds + # for now using token from ini + #auth = self.get_admin_auth_token("admin", "secrete", "1") + #admin_token = json.loads(auth)["auth"]["token"]["id"] + + # Step 2: validate the user's token with the auth service + # since this is a priviledged op,m we need to auth ourselves + # by using an admin token + headers = {"Content-type": "application/json", + "Accept": "text/json", + "X-Auth-Token": self.admin_token} + ##TODO:we need to figure out how to auth to keystone + #since validate_token is a priviledged call + #Khaled's version uses creds to get a token + # "X-Auth-Token": admin_token} + # we're using a test token from the ini file for now + conn = http_connect(self.auth_host, self.auth_port, 'GET', + '/v1.0/token/%s' % token, headers=headers) + resp = conn.getresponse() + data = resp.read() + conn.close() + + if not str(resp.status).startswith('20'): + # Keystone rejected claim + if self.delay_auth_decision: + # Downstream service will receive call still and decide + _decorate_request_headers("X_IDENTITY_STATUS", "Invalid", + proxy_headers, env) + else: + # Reject the response & send back the error + # (not delay_auth_decision) + return HTTPUnauthorized(headers=headers)(env, + start_response) + else: + # Valid token. Get user data and put it in to the call + # so the downstream service can use iot + dict_response = json.loads(data) + #TODO(Ziad): make this more robust + user = dict_response['auth']['user']['username'] + tenant = dict_response['auth']['user']['tenantId'] + group = '%s/%s' % (dict_response['auth']['user']['groups']['group'][0]['id'], + dict_response['auth']['user']['groups']['group'][0]['tenantId']) + + # TODO(Ziad): add additional details we may need, + # like tenant and group info + _decorate_request_headers('X_AUTHORIZATION', "Proxy %s" % user, + proxy_headers, env) + _decorate_request_headers("X_IDENTITY_STATUS", "Confirmed", + proxy_headers, env) + _decorate_request_headers('X_TENANT', tenant, + proxy_headers, env) + _decorate_request_headers('X_GROUP', group, + proxy_headers, env) + + #Token/Auth processed, headers added now decide how to pass on the call + _decorate_request_headers('AUTHORIZATION', + "Basic %s" % self.service_pass, + proxy_headers, + env) + if self.app: + # Pass to downstream WSGI component + return self.app(env, custom_start_response) + else: + # We are forwarding to a remote service (no downstream WSGI app) + req = Request(proxy_headers) + parsed = urlparse(req.url) + conn = http_connect(self.service_host, self.service_port, \ + req.method, parsed.path, \ + proxy_headers,\ + ssl=(self.service_protocol == 'https')) + resp = conn.getresponse() + data = resp.read() + #TODO: use a more sophisticated proxy + # we are rewriting the headers now + return Response(status=resp.status, body=data)(proxy_headers, + start_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) + +if __name__ == "__main__": + app = loadapp("config:" + \ + os.path.join(os.path.abspath(os.path.dirname(__file__)), + "auth_token.ini"), global_conf={"log_name": "auth_token.log"}) + wsgi.server(eventlet.listen(('', 8090)), app) diff --git a/keystone/common/__init__.py b/keystone/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone/common/bufferedhttp.py b/keystone/common/bufferedhttp.py new file mode 100644 index 00000000..fdb35ee6 --- /dev/null +++ b/keystone/common/bufferedhttp.py @@ -0,0 +1,165 @@ +# Copyright (c) 2010-2011 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. + +""" +Monkey Patch httplib.HTTPResponse to buffer reads of headers. This can improve +performance when making large numbers of small HTTP requests. This module +also provides helper functions to make HTTP connections using +BufferedHTTPResponse. + +.. warning:: + + If you use this, be sure that the libraries you are using do not access + the socket directly (xmlrpclib, I'm looking at you :/), and instead + make all calls through httplib. +""" + +from urllib import quote +import logging +import time + +from eventlet.green.httplib import CONTINUE, HTTPConnection, HTTPMessage, \ + HTTPResponse, HTTPSConnection, _UNKNOWN + + +class BufferedHTTPResponse(HTTPResponse): + """HTTPResponse class that buffers reading of headers""" + + def __init__(self, sock, debuglevel=0, strict=0, + method=None): # pragma: no cover + self.sock = sock + self.fp = sock.makefile('rb') + self.debuglevel = debuglevel + self.strict = strict + self._method = method + + self.msg = None + + # from the Status-Line of the response + self.version = _UNKNOWN # HTTP-Version + self.status = _UNKNOWN # Status-Code + self.reason = _UNKNOWN # Reason-Phrase + + self.chunked = _UNKNOWN # is "chunked" being used? + self.chunk_left = _UNKNOWN # bytes left to read in current chunk + self.length = _UNKNOWN # number of bytes left in response + self.will_close = _UNKNOWN # conn will close at end of response + + def expect_response(self): + self.fp = self.sock.makefile('rb', 0) + version, status, reason = self._read_status() + if status != CONTINUE: + self._read_status = lambda: (version, status, reason) + self.begin() + else: + self.status = status + self.reason = reason.strip() + self.version = 11 + self.msg = HTTPMessage(self.fp, 0) + self.msg.fp = None + + +class BufferedHTTPConnection(HTTPConnection): + """HTTPConnection class that uses BufferedHTTPResponse""" + response_class = BufferedHTTPResponse + + def connect(self): + self._connected_time = time.time() + return HTTPConnection.connect(self) + + def putrequest(self, method, url, skip_host=0, skip_accept_encoding=0): + self._method = method + self._path = url + return HTTPConnection.putrequest(self, method, url, skip_host, + skip_accept_encoding) + + def getexpect(self): + response = BufferedHTTPResponse(self.sock, strict=self.strict, + method=self._method) + response.expect_response() + return response + + def getresponse(self): + response = HTTPConnection.getresponse(self) + logging.debug(("HTTP PERF: %(time).5f seconds to %(method)s " + "%(host)s:%(port)s %(path)s)"), + {'time': time.time() - self._connected_time, 'method': self._method, + 'host': self.host, 'port': self.port, 'path': self._path}) + return response + + +def http_connect(ipaddr, port, device, partition, method, path, + headers=None, query_string=None, ssl=False): + """ + Helper function to create an HTTPConnection object. If ssl is set True, + HTTPSConnection will be used. However, if ssl=False, BufferedHTTPConnection + will be used, which is buffered for backend Swift services. + + :param ipaddr: IPv4 address to connect to + :param port: port to connect to + :param device: device of the node to query + :param partition: partition on the device + :param method: HTTP method to request ('GET', 'PUT', 'POST', etc.) + :param path: request path + :param headers: dictionary of headers + :param query_string: request query string + :param ssl: set True if SSL should be used (default: False) + :returns: HTTPConnection object + """ + if ssl: + conn = HTTPSConnection('%s:%s' % (ipaddr, port)) + else: + conn = BufferedHTTPConnection('%s:%s' % (ipaddr, port)) + path = quote('/' + device + '/' + str(partition) + path) + if query_string: + path += '?' + query_string + conn.path = path + conn.putrequest(method, path) + if headers: + for header, value in headers.iteritems(): + conn.putheader(header, value) + conn.endheaders() + return conn + + +def http_connect_raw(ipaddr, port, method, path, headers=None, + query_string=None, ssl=False): + """ + Helper function to create an HTTPConnection object. If ssl is set True, + HTTPSConnection will be used. However, if ssl=False, BufferedHTTPConnection + will be used, which is buffered for backend Swift services. + + :param ipaddr: IPv4 address to connect to + :param port: port to connect to + :param method: HTTP method to request ('GET', 'PUT', 'POST', etc.) + :param path: request path + :param headers: dictionary of headers + :param query_string: request query string + :param ssl: set True if SSL should be used (default: False) + :returns: HTTPConnection object + """ + if ssl: + conn = HTTPSConnection('%s:%s' % (ipaddr, port)) + else: + conn = BufferedHTTPConnection('%s:%s' % (ipaddr, port)) + if query_string: + path += '?' + query_string + conn.path = path + conn.putrequest(method, path) + if headers: + for header, value in headers.iteritems(): + conn.putheader(header, value) + conn.endheaders() + return conn diff --git a/keystone/content/extensions.json b/keystone/content/extensions.json new file mode 100644 index 00000000..9e1c96d0 --- /dev/null +++ b/keystone/content/extensions.json @@ -0,0 +1 @@ +{ "extensions" : { "values" : []}} \ No newline at end of file diff --git a/keystone/content/extensions.xml b/keystone/content/extensions.xml new file mode 100644 index 00000000..ed5ee9c6 --- /dev/null +++ b/keystone/content/extensions.xml @@ -0,0 +1,5 @@ + + + + diff --git a/keystone/content/idmdevguide.pdf b/keystone/content/idmdevguide.pdf new file mode 100644 index 00000000..3c37c310 Binary files /dev/null and b/keystone/content/idmdevguide.pdf differ diff --git a/keystone/content/version.json.tpl b/keystone/content/version.json.tpl new file mode 100644 index 00000000..8fe0e2e1 --- /dev/null +++ b/keystone/content/version.json.tpl @@ -0,0 +1,33 @@ +{ + "version" : { + "id" : "v1.0", + "status" : "{{VERSION_STATUS}}", + "updated" : "{{VERSION_DATE}}", + "links": [ + { + "rel" : "self", + "href" : "http://{{HOST}}:{{PORT}}/v1.0/" + }, + { + "rel" : "describedby", + "type" : "application/pdf", + "href" : "http://{{HOST}}:{{PORT}}/v1.0/idmdevguide.pdf" + }, + { + "rel" : "describedby", + "type" : "application/vnd.sun.wadl+xml", + "href" : "http://{{HOST}}:{{PORT}}/v1.0/identity.wadl" + } + ], + "media-types": [ + { + "base" : "application/xml", + "type" : "application/vnd.openstack.idm-v1.0+xml" + }, + { + "base" : "application/json", + "type" : "application/vnd.openstack.idm-v1.0+json" + } + ] + } +} diff --git a/keystone/content/version.xml.tpl b/keystone/content/version.xml.tpl new file mode 100644 index 00000000..a7ee96b9 --- /dev/null +++ b/keystone/content/version.xml.tpl @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/keystone/db/__init__.py b/keystone/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone/db/sqlalchemy/__init__.py b/keystone/db/sqlalchemy/__init__.py new file mode 100644 index 00000000..cffaa881 --- /dev/null +++ b/keystone/db/sqlalchemy/__init__.py @@ -0,0 +1,24 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2010-2011 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. +# Not Yet PEP8 standardized + + +from models import register_models +from session import get_session, get_engine + +session = get_session() +engine = get_engine() +register_models(session, engine) diff --git a/keystone/db/sqlalchemy/api.py b/keystone/db/sqlalchemy/api.py new file mode 100644 index 00000000..a0910778 --- /dev/null +++ b/keystone/db/sqlalchemy/api.py @@ -0,0 +1,179 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2010-2011 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. +# Not Yet PEP8 standardized + + +from session import get_session +from sqlalchemy.orm import joinedload +import models + + +def tenant_create(values): + tenant_ref = models.Tenant() + tenant_ref.update(values) + tenant_ref.save() + return tenant_ref + + +def tenant_get(id, session=None): + if not session: + session = get_session() + result = session.query(models.Tenant).filter_by(id=id).first() + return result + + +def tenant_get_all(session=None): + if not session: + session = get_session() + return session.query(models.Tenant).all() + + +def tenant_is_empty(id, session=None): + if not session: + session = get_session() + a_user = session.query(models.UserTenantAssociation).filter_by( + tenant_id=id).first() + if a_user != None: + return False + a_group = session.query(models.Group).filter_by(tenant_id=id).first() + if a_group != None: + return False + return True + + +def tenant_update(id, values, session=None): + if not session: + session = get_session() + with session.begin(): + tenant_ref = tenant_get(id, session) + tenant_ref.update(values) + tenant_ref.save(session=session) + + +def tenant_delete(id, session=None): + if not session: + session = get_session() + with session.begin(): + tenant_ref = tenant_get(id, session) + session.delete(tenant_ref) + + +def user_create(values): + user_ref = models.User() + user_ref.update(values) + user_ref.save() + return user_ref + + +def user_get(id, session=None): + if not session: + session = get_session() + result = session.query(models.User).options(joinedload('groups')).options( + joinedload('tenants')).filter_by(id=id).first() + return result + + +def user_get_by_tenant(tenant_id, session=None): + if not session: + session = get_session() + result = session.query(models.UserTenantAssociation).filter_by( + tenant_id=tenant_id) + return result + + +def user_groups(id, session=None): + if not session: + session = get_session() + result = session.query(models.Group).filter_by( + user_id=id) + return result + + +def user_update(id, values, session=None): + if not session: + session = get_session() + with session.begin(): + user_ref = user_get(id, session) + user_ref.update(values) + user_ref.save(session=session) + + +def user_delete(id, session=None): + if not session: + session = get_session() + with session.begin(): + user_ref = user_get(id, session) + session.delete(user_ref) + + +def group_get(id, session=None): + if not session: + session = get_session() + result = session.query(models.Group).filter_by(id=id).first() + return result + + +def group_users(id, session=None): + if not session: + session = get_session() + result = session.query(models.Users).filter_by( + group_id=id) + return result + + +def group_get_all(session=None): + if not session: + session = get_session() + result = session.query(models.Group) + return result + + +def group_delete(id, session=None): + if not session: + session = get_session() + with session.begin(): + group_ref = group_get(id, session) + session.delete(group_ref) + + +def token_create(values): + token_ref = models.Token() + token_ref.update(values) + token_ref.save() + return token_ref + + +def token_get(id, session=None): + if not session: + session = get_session() + result = session.query(models.Token).filter_by(token_id=id).first() + return result + + +def token_delete(id, session=None): + if not session: + session = get_session() + with session.begin(): + token_ref = token_get(id, session) + session.delete(token_ref) + + +def token_for_user(user_id, session=None): + if not session: + session = get_session() + result = session.query(models.Token).filter_by( + user_id=user_id).order_by("expires desc").first() + return result diff --git a/keystone/db/sqlalchemy/models.py b/keystone/db/sqlalchemy/models.py new file mode 100644 index 00000000..f8050377 --- /dev/null +++ b/keystone/db/sqlalchemy/models.py @@ -0,0 +1,135 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2010-2011 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. +# Not Yet PEP8 standardized + +from sqlalchemy import create_engine, Column, String, Integer, ForeignKey +from sqlalchemy import DateTime +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship, object_mapper + +from session import get_session + +Base = declarative_base() + + +class KeystoneBase(object): + """Base class for Keystone Models.""" + + def save(self, session=None): + """Save this object.""" + if not session: + session = get_session() + session.add(self) + try: + session.flush() + except IntegrityError: + raise + + def delete(self, session=None): + """Delete this object.""" + self.save(session=session) + + def __setitem__(self, key, value): + setattr(self, key, value) + + def __getitem__(self, key): + return getattr(self, key) + + def get(self, key, default=None): + return getattr(self, key, default) + + def __iter__(self): + self._i = iter(object_mapper(self).columns) + return self + + def next(self): + n = self._i.next().name + return n, getattr(self, n) + + def update(self, values): + """Make the model object behave like a dict""" + for k, v in values.iteritems(): + setattr(self, k, v) + + def iteritems(self): + """Make the model object behave like a dict. + + Includes attributes from joins.""" + local = dict(self) + joined = dict([(k, v) for k, v in self.__dict__.iteritems() + if not k[0] == '_']) + local.update(joined) + return local.iteritems() + + +class UserTenantAssociation(Base, KeystoneBase): + __tablename__ = 'user_tenant_association' + + user_id = Column(String(255), ForeignKey('users.id'), primary_key=True) + tenant_id = Column(String(255), ForeignKey('tenants.id'), primary_key=True) + + +class UserGroupAssociation(Base, KeystoneBase): + __tablename__ = 'user_group_association' + + user_id = Column(String(255), ForeignKey('users.id'), primary_key=True) + group_id = Column(String(255), ForeignKey('groups.id'), primary_key=True) + + +class User(Base, KeystoneBase): + __tablename__ = 'users' + + id = Column(String(255), primary_key=True, unique=True) + password = Column(String(255)) + email = Column(String(255)) + enabled = Column(Integer) + groups = relationship(UserGroupAssociation, backref='users') + tenants = relationship(UserTenantAssociation, backref='user') + + +class Tenant(Base, KeystoneBase): + __tablename__ = 'tenants' + + id = Column(String(255), primary_key=True, unique=True) + desc = Column(String(255)) + enabled = Column(Integer) + groups = relationship('Group', backref='tenants') + + +class Group(Base, KeystoneBase): + __tablename__ = 'groups' + + id = Column(String(255), primary_key=True, unique=True) + desc = Column(String(255)) + tenant_id = Column(String(255), ForeignKey('tenants.id')) + + +class Token(Base, KeystoneBase): + __tablename__ = 'token' + + token_id = Column(String(255), primary_key=True, unique=True) + user_id = Column(String(255)) + tenant_id = Column(String(255)) + expires = Column(DateTime) + + +def register_models(session, engine): + models = (User, Tenant, Group, Token, UserGroupAssociation, + UserTenantAssociation) + for model in models: + model.metadata.create_all(engine) + session.flush() diff --git a/keystone/db/sqlalchemy/session.py b/keystone/db/sqlalchemy/session.py new file mode 100644 index 00000000..10e727a7 --- /dev/null +++ b/keystone/db/sqlalchemy/session.py @@ -0,0 +1,64 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Session Handling for SQLAlchemy backend +""" +import logging +import os + +from sqlalchemy import create_engine +from sqlalchemy import pool +from sqlalchemy.orm import sessionmaker + + +_ENGINE = None +_MAKER = None + + +def get_connection_string(): + path = os.path.realpath(__file__) + dbpath = os.path.normpath(os.path.join(path, + os.pardir, # sqlalchemy + os.pardir, # db + os.pardir # keystone + )) + connection_string = "sqlite:///%s/keystone.db" % dbpath + logging.debug('SQL ALchemy connection string: %s', connection_string) + return connection_string + + +def get_session(autocommit=True, expire_on_commit=False): + """Helper method to grab session""" + global _ENGINE + global _MAKER + if not _MAKER: + if not _ENGINE: + kwargs = {'pool_recycle': 30, 'echo': False} + kwargs['poolclass'] = pool.NullPool # for SQLite3 + _ENGINE = create_engine(get_connection_string(), **kwargs) + _MAKER = (sessionmaker(bind=_ENGINE, + autocommit=autocommit, + expire_on_commit=expire_on_commit)) + session = _MAKER() + return session + + +def get_engine(): + if not _ENGINE: + raise + return _ENGINE diff --git a/keystone/identity.py b/keystone/identity.py new file mode 100644 index 00000000..8bb6a4f7 --- /dev/null +++ b/keystone/identity.py @@ -0,0 +1,302 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2010-2011 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. + + +""" +Service that stores identities and issues and manages tokens + +HEADERS +------- +HTTP_ is a standard http header +HTTP_X is an extended http header + +> Coming in from initial call +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 cloud files +> Used for communication between components +www-authenticate : only used if this component is being used remotely +HTTP_AUTHORIZATION : basic auth password used to validate the connection + +> What we add to the request for use by the OpenStack service +HTTP_X_AUTHORIZATION: the client identity being passed in + +""" + +import functools +import logging +import os +import sys +import eventlet +from eventlet import wsgi + +import bottle +from bottle import request +from bottle import response + +# If ../keystone/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'keystone', '__init__.py')): + sys.path.insert(0, POSSIBLE_TOPDIR) + +import keystone.logic.service as serv +import keystone.logic.types.auth as auth +import keystone.logic.types.tenant as tenants +import keystone.logic.types.fault as fault + +VERSION_STATUS = "ALPHA" +VERSION_DATE = "2011-04-23T00:00:00Z" + +bottle.debug(True) + +service = serv.IDMService() + +## +## Override error pages +## + + +@bottle.error(400) +@bottle.error(401) +@bottle.error(403) +@bottle.error(404) +@bottle.error(409) +@bottle.error(415) +@bottle.error(500) +@bottle.error(503) +def error_handler(err): + return err.output + + +def is_xml_response(): + if not "Accept" in request.header: + return False + return request.header["Accept"] == "application/xml" + + +def get_app_root(): + return os.path.abspath(os.path.dirname(__file__)) + + +def send_result(code, result): + content = None + response.content_type = None + if result: + if is_xml_response(): + content = result.to_xml() + response.content_type = "application/xml" + else: + content = result.to_json() + response.content_type = "application/json" + response.status = code + if code > 399: + return bottle.abort(code, content) + return content + + +def get_normalized_request_content(model): + """initialize a model from json/xml contents of request body""" + + ctype = request.environ.get("CONTENT_TYPE") + if ctype == "application/xml": + ret = model.from_xml(request.body.read()) + elif ctype == "application/json": + ret = model.from_json(request.body.read()) + else: + raise fault.IDMFault("I don't understand the content type ", code=415) + return ret + + +def get_auth_token(): + auth_token = None + if "X-Auth-Token" in request.header: + auth_token = request.header["X-Auth-Token"] + return auth_token + + +def wrap_error(func): + @functools.wraps(func) + def check_error(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as err: + if isinstance(err, fault.IDMFault): + send_result(err.code, err) + else: + logging.exception(err) + send_result(500, fault.IDMFault("Unhandled error", str(err))) + return check_error + + +@bottle.route('/v1.0', method='GET') +@bottle.route('/v1.0/', method='GET') +@wrap_error +def get_version_info(): + if is_xml_response(): + resp_file = "content/version.xml" + response.content_type = "application/xml" + else: + resp_file = "content/version.json" + response.content_type = "application/json" + hostname = request.environ.get("SERVER_NAME") + port = request.environ.get("SERVER_PORT") + return bottle.template(resp_file, HOST=hostname, PORT=port, + VERSION_STATUS=VERSION_STATUS, + VERSION_DATE=VERSION_DATE) + +## +## Version links: +## + + +@bottle.route('/v1.0/idmdevguide.pdf', method='GET') +@wrap_error +def get_pdf_contract(): + return bottle.static_file("content/idmdevguide.pdf", + root=get_app_root(), + mimetype="application/pdf") + + +@bottle.route('/v1.0/identity.wadl', method='GET') +@wrap_error +def get_wadl_contract(): + return bottle.static_file("identity.wadl", + root=get_app_root(), + mimetype="application/vnd.sun.wadl+xml") + + +@bottle.route('/v1.0/xsd/:xsd', method='GET') +@wrap_error +def get_xsd_contract(xsd): + return bottle.static_file("/xsd/" + xsd, + root=get_app_root(), + mimetype="application/xml") + + +@bottle.route('/v1.0/xsd/atom/:xsd', method='GET') +@wrap_error +def get_xsd_atom_contract(xsd): + return bottle.static_file("/xsd/atom/" + xsd, + root=get_app_root(), + mimetype="application/xml") + +## +## Token Operations +## + + +@bottle.route('/v1.0/token', method='POST') +@wrap_error +def authenticate(): + creds = get_normalized_request_content(auth.PasswordCredentials) + return send_result(200, service.authenticate(creds)) + + +@bottle.route('/v1.0/token/:token_id', method='GET') +@wrap_error +def validate_token(token_id): + belongs_to = None + if "belongsTo" in request.GET: + belongs_to = request.GET["belongsTo"] + rval = service.validate_token(get_auth_token(), token_id, belongs_to) + return send_result(200, rval) + + +@bottle.route('/v1.0/token/:token_id', method='DELETE') +@wrap_error +def delete_token(token_id): + return send_result(204, + service.revoke_token(get_auth_token(), token_id)) + +## +## Tenant Operations +## + +@bottle.route('/v1.0/tenants', method='POST') +@wrap_error +def create_tenant(): + tenant = get_normalized_request_content(tenants.Tenant) + return send_result(201, + service.create_tenant(get_auth_token(), tenant)) + + +@bottle.route('/v1.0/tenants', method='GET') +@wrap_error +def get_tenants(): + marker = None + if "marker" in request.GET: + marker = request.GET["marker"] + limit = None + if "limit" in request.GET: + limit = request.GET["limit"] + tenants = service.get_tenants(get_auth_token(), marker, limit) + return send_result(200, tenants) + + +@bottle.route('/v1.0/tenants/:tenant_id', method='GET') +@wrap_error +def get_tenant(tenant_id): + tenant = service.get_tenant(get_auth_token(), tenant_id) + return send_result(200, tenant) + + +@bottle.route('/v1.0/tenants/:tenant_id', method='PUT') +@wrap_error +def update_tenant(tenant_id): + tenant = get_normalized_request_content(tenants.Tenant) + rval = service.update_tenant(get_auth_token(), tenant_id, tenant) + return send_result(200, rval) + + +@bottle.route('/v1.0/tenants/:tenant_id', method='DELETE') +@wrap_error +def delete_tenant(tenant_id): + rval = service.delete_tenant(get_auth_token(), tenant_id) + return send_result(204, rval) + + +## +## Extensions +## + +@bottle.route('/v1.0/extensions', method='GET') +@wrap_error +def get_extensions(): + if is_xml_response(): + resp_file = "content/extensions.xml" + mimetype = "application/xml" + else: + resp_file = "content/extensions.json" + mimetype = "application/json" + return bottle.static_file(resp_file, + root=get_app_root(), + mimetype=mimetype) + + +@bottle.route('/v1.0/extensions/:ext_alias', method='GET') +@wrap_error +def get_extension(ext_alias): + # + # Todo: Define some extensions :-) + # + raise fault.ItemNotFoundFault("The extension is not found") + + +if __name__ == "__main__": + wsgi.server(eventlet.listen(('', 8080)), bottle.default_app()) diff --git a/keystone/identity.wadl b/keystone/identity.wadl new file mode 120000 index 00000000..4c93e9e5 --- /dev/null +++ b/keystone/identity.wadl @@ -0,0 +1 @@ +../docs/guide/src/docbkx/idm.wadl \ No newline at end of file diff --git a/keystone/keystone.ini b/keystone/keystone.ini new file mode 100644 index 00000000..626ca1ed --- /dev/null +++ b/keystone/keystone.ini @@ -0,0 +1,16 @@ +; +; This file not used +; +[DEFAULT] + +[composite:main] +use = egg:Paste#urlmap +/ = home +/tenants = admin + +[app:home] +use = egg:keystone + +[app:admin] +use = egg:keystone + diff --git a/keystone/logic/__init__.py b/keystone/logic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone/logic/service.py b/keystone/logic/service.py new file mode 100644 index 00000000..cd0b5237 --- /dev/null +++ b/keystone/logic/service.py @@ -0,0 +1,216 @@ +# Copyright (c) 2010-2011 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. + +from datetime import datetime +from datetime import timedelta + +import keystone.logic.types.auth as auth +import keystone.logic.types.tenant as tenants +import keystone.logic.types.atom as atom +import keystone.logic.types.fault as fault + +import keystone.db.sqlalchemy.api as db_api +import keystone.db.sqlalchemy.models as db_models + +import uuid + + +class IDMService(object): + "This is the logical implemenation of the IDM service" + + # + # Token Operations + # + def authenticate(self, credentials): + if not isinstance(credentials, auth.PasswordCredentials): + raise fault.BadRequestFault("Expecting Password Credentials!") + + duser = db_api.user_get(credentials.username) + if duser == None: + raise fault.UnauthorizedFault("Unauthorized") + if not duser.enabled: + raise fault.UserDisabledFault("Your account has been disabled") + if duser.password != credentials.password: + raise fault.UnauthorizedFault("Unauthorized") + + # + # Look for an existing token, or create one, + # TODO: Handle tenant/token search + # + dtoken = db_api.token_for_user(duser.id) + if not dtoken or dtoken.expires < datetime.now(): + dtoken = db_models.Token() + dtoken.token_id = str(uuid.uuid4()) + dtoken.user_id = duser.id + if not duser.tenants: + raise fault.IDMFault("Strange: user %s is not associated " + "with a tenant!" % duser.id) + dtoken.tenant_id = duser.tenants[0].tenant_id + dtoken.expires = datetime.now() + timedelta(days=1) + + db_api.token_create(dtoken) + + return self.__get_auth_data(dtoken, duser) + + def validate_token(self, admin_token, token_id, belongs_to=None): + self.__validate_token(admin_token) + + (dtoken, duser) = self.__get_dauth_data(token_id) + + if not dtoken: + raise fault.ItemNotFoundFault("Token not found") + + if dtoken.expires < datetime.now(): + raise fault.ItemNotFoundFault("Token not found") + + if belongs_to != None and dtoken.tenant_id != belongs_to: + raise fault.ItemNotFoundFault("Token not found") + + return self.__get_auth_data(dtoken, duser) + + def revoke_token(self, admin_token, token_id): + self.__validate_token(admin_token) + + dtoken = db_api.token_get(token_id) + if not dtoken: + raise fault.ItemNotFoundFault("Token not found") + + db_api.token_delete(token_id) + + # + # Tenant Operations + # + def create_tenant(self, admin_token, tenant): + self.__validate_token(admin_token) + + if not isinstance(tenant, tenants.Tenant): + raise fault.BadRequestFault("Expecting a Tenant") + + if tenant.tenant_id == None: + raise fault.BadRequestFault("Expecting a unique Tenant Id") + + if db_api.tenant_get(tenant.tenant_id) != None: + raise fault.TenantConflictFault( + "A tenant with that id already exists") + + dtenant = db_models.Tenant() + dtenant.id = tenant.tenant_id + dtenant.desc = tenant.description + dtenant.enabled = tenant.enabled + + db_api.tenant_create(dtenant) + + return tenant + + def get_tenants(self, admin_token, marker, limit): + self.__validate_token(admin_token) + + ts = [] + dtenants = db_api.tenant_get_all() + for dtenant in dtenants: + ts.append(tenants.Tenant(dtenant.id, + dtenant.desc, dtenant.enabled)) + + return tenants.Tenants(ts, []) + + def get_tenant(self, admin_token, tenant_id): + self.__validate_token(admin_token) + + dtenant = db_api.tenant_get(tenant_id) + if not dtenant: + raise fault.ItemNotFoundFault("The tenant could not be found") + + return tenants.Tenant(dtenant.id, dtenant.desc, dtenant.enabled) + + def update_tenant(self, admin_token, tenant_id, tenant): + self.__validate_token(admin_token) + + if not isinstance(tenant, tenants.Tenant): + raise fault.BadRequestFault("Expecting a Tenant") + True + + dtenant = db_api.tenant_get(tenant_id) + if dtenant == None: + raise fault.ItemNotFoundFault("The tenant cloud not be found") + + values = {'desc': tenant.description, 'enabled': tenant.enabled} + + db_api.tenant_update(tenant_id, values) + + return tenants.Tenant(dtenant.id, tenant.description, tenant.enabled) + + def delete_tenant(self, admin_token, tenant_id): + self.__validate_token(admin_token) + + dtenant = db_api.tenant_get(tenant_id) + if dtenant == None: + raise fault.ItemNotFoundFault("The tenant cloud not be found") + + if not db_api.tenant_is_empty(tenant_id): + raise fault.ForbiddenFault("You may not delete a tenant that " + "contains users or groups") + + db_api.tenant_delete(dtenant.id) + return None + + # + # Private Operations + # + def __get_dauth_data(self, token_id): + """return token and user object for a token_id""" + + token = None + user = None + if token_id: + token = db_api.token_get(token_id) + if token: + user = db_api.user_get(token.user_id) + return (token, user) + + def __get_auth_data(self, dtoken, duser): + """return AuthData object for a token/user pair""" + + token = auth.Token(dtoken.expires, dtoken.token_id) + + gs = [] + for ug in duser.groups: + dgroup = db_api.group_get(ug.group_id) + gs.append(auth.Group(dgroup.id, dgroup.tenant_id)) + groups = auth.Groups(gs, []) + if len(duser.tenants) == 0: + raise fault.IDMFault("Strange: user %s is not associated " + "with a tenant!" % duser.id) + user = auth.User(duser.id, duser.tenants[0].tenant_id, groups) + return auth.AuthData(token, user) + + def __validate_token(self, token_id, admin=True): + if not token_id: + raise fault.UnauthorizedFault("Missing token") + (token, user) = self.__get_dauth_data(token_id) + + if not token: + raise fault.UnauthorizedFault("Bad token, please reauthenticate") + if token.expires < datetime.now(): + raise fault.UnauthorizedFault("Token expired, please renew") + if not user.enabled: + raise fault.UserDisabledFault("The user %s has been disabled!" + % user.id) + if admin: + for ug in user.groups: + if ug.group_id == "Admin": + return (token, user) + raise fault.ForbiddenFault("You are not authorized " + "to make this call") + return (token, user) diff --git a/keystone/logic/types/__init__.py b/keystone/logic/types/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone/logic/types/atom.py b/keystone/logic/types/atom.py new file mode 100644 index 00000000..04431ce2 --- /dev/null +++ b/keystone/logic/types/atom.py @@ -0,0 +1,25 @@ +# Copyright (c) 2010-2011 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. + + +class Link(object): + "An atom link" + + def __init__(self, rel, href, link_type=None, hreflang=None, title=None): + self.rel = rel + self.href = href + self.link_type = link_type + self.hreflang = hreflang + self.title = title diff --git a/keystone/logic/types/auth.py b/keystone/logic/types/auth.py new file mode 100644 index 00000000..fc5be63a --- /dev/null +++ b/keystone/logic/types/auth.py @@ -0,0 +1,157 @@ +# Copyright (c) 2010-2011 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. + +from datetime import datetime + +from abc import ABCMeta +import json +import keystone.logic.types.fault as fault +from lxml import etree + + +class PasswordCredentials(object): + "Credentials based on username, password, and (optional) tenant_id." + + def __init__(self, username, password, tenant_id): + self.username = username + self.password = password + self.tenant_id = tenant_id + + @staticmethod + def from_xml(xml_str): + try: + dom = etree.Element("root") + dom.append(etree.fromstring(xml_str)) + root = dom.find("{http://docs.openstack.org/idm/api/v1.0}" + "passwordCredentials") + if root == None: + raise fault.BadRequestFault("Expecting passwordCredentials") + username = root.get("username") + if username == None: + raise fault.BadRequestFault("Expecting a username") + password = root.get("password") + if password == None: + raise fault.BadRequestFault("Expecting a password") + tenant_id = root.get("tenantId") + return PasswordCredentials(username, password, tenant_id) + except etree.LxmlError as e: + raise fault.BadRequestFault("Cannot parse password credentials", + str(e)) + + @staticmethod + def from_json(json_str): + try: + obj = json.loads(json_str) + if not "passwordCredentials" in obj: + raise fault.BadRequestFault("Expecting passwordCredentials") + cred = obj["passwordCredentials"] + if not "username" in cred: + raise fault.BadRequestFault("Expecting a username") + username = cred["username"] + if not "password" in cred: + raise fault.BadRequestFault("Expecting a password") + password = cred["password"] + if "tenantId" in cred: + tenant_id = cred["tenantId"] + else: + tenant_id = None + return PasswordCredentials(username, password, tenant_id) + except (ValueError, TypeError) as e: + raise fault.BadRequestFault("Cannot parse password credentials", + str(e)) + + +class Token(object): + "An auth token." + + def __init__(self, expires, token_id): + self.expires = expires + self.token_id = token_id + + +class Group(object): + "A group, optionally belonging to a tenant." + + def __init__(self, group_id, tenant_id): + self.tenant_id = tenant_id + self.group_id = group_id + + +class Groups(object): + "A collection of groups." + + def __init__(self, values, links): + self.values = values + self.links = links + + +class User(object): + "A user." + + def __init__(self, username, tenant_id, groups): + self.username = username + self.tenant_id = tenant_id + self.groups = groups + + +class AuthData(object): + "Authentation Infor returned upon successful login." + + def __init__(self, token, user): + self.token = token + self.user = user + + def to_xml(self): + dom = etree.Element("auth", + xmlns="http://docs.openstack.org/idm/api/v1.0") + token = etree.Element("token", + expires=self.token.expires.isoformat()) + token.set("id", self.token.token_id) + user = etree.Element("user", + username=self.user.username, + tenantId=self.user.tenant_id) + groups = etree.Element("groups") + for group in self.user.groups.values: + g = etree.Element("group", + tenantId=group.tenant_id) + g.set("id", group.group_id) + groups.append(g) + user.append(groups) + dom.append(token) + dom.append(user) + return etree.tostring(dom) + + def to_json(self): + token = {} + token["id"] = self.token.token_id + token["expires"] = self.token.expires.isoformat() + user = {} + user["username"] = self.user.username + user["tenantId"] = self.user.tenant_id + group = [] + for g in self.user.groups.values: + grp = {} + grp["tenantId"] = g.tenant_id + grp["id"] = g.group_id + group.append(grp) + groups = {} + groups["group"] = group + user["groups"] = groups + auth = {} + auth["token"] = token + auth["user"] = user + ret = {} + ret["auth"] = auth + return json.dumps(ret) diff --git a/keystone/logic/types/fault.py b/keystone/logic/types/fault.py new file mode 100644 index 00000000..29bd2dc9 --- /dev/null +++ b/keystone/logic/types/fault.py @@ -0,0 +1,121 @@ +# Copyright (c) 2010-2011 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 json +from lxml import etree + + +class IDMFault(Exception): + "Base Exception type for all auth exceptions" + + def __init__(self, msg, details=None, code=500): + self.args = (code, msg, details) + self.code = code + self.msg = msg + self.details = details + self.key = "idmFault" + + @property + def message(self): + return self.msg + + def to_xml(self): + dom = etree.Element(self.key, + xmlns="http://docs.openstack.org/idm/api/v1.0") + dom.set("code", str(self.code)) + msg = etree.Element("message") + msg.text = self.msg + dom.append(msg) + if self.details != None: + desc = etree.Element("details") + desc.text = self.details + dom.append(desc) + return etree.tostring(dom) + + def to_json(self): + fault = {} + fault["message"] = self.msg + fault["code"] = str(self.code) + if self.details != None: + fault["details"] = self.details + ret = {} + ret[self.key] = fault + return json.dumps(ret) + + +class ServiceUnavailableFault(IDMFault): + "The auth service is unavailable" + + def __init__(self, msg, details=None, code=503): + super(ServiceUnavailableFault, self).__init__(msg, details, code) + self.key = "serviceUnavailable" + + +class BadRequestFault(IDMFault): + "Bad user request" + + def __init__(self, msg, details=None, code=400): + super(BadRequestFault, self).__init__(msg, details, code) + self.key = "badRequest" + + +class UnauthorizedFault(IDMFault): + "User is unauthorized" + + def __init__(self, msg, details=None, code=401): + super(UnauthorizedFault, self).__init__(msg, details, code) + self.key = "unauthorized" + + +class UserDisabledFault(IDMFault): + "The user is disabled" + + def __init__(self, msg, details=None, code=403): + super(UserDisabledFault, self).__init__(msg, details, code) + self.key = "userDisabled" + + +class ForbiddenFault(IDMFault): + "The user is forbidden" + + def __init__(self, msg, details=None, code=403): + super(ForbiddenFault, self).__init__(msg, details, code) + self.key = "forbidden" + + +class ItemNotFoundFault(IDMFault): + "The item is not found" + + def __init__(self, msg, details=None, code=404): + super(ItemNotFoundFault, self).__init__(msg, details, code) + self.key = "itemNotFound" + + +class TenantConflictFault(IDMFault): + "The tenant already exists?" + + def __init__(self, msg, details=None, code=409): + super(TenantConflictFault, self).__init__(msg, details, code) + self.key = "tenantConflict" + + +class OverlimitFault(IDMFault): + "A limit has been exceeded" + + def __init__(self, msg, details=None, code=409, retry_at=None): + super(OverlimitFault, self).__init__(msg, details, code) + self.args = (code, msg, details, retry_at) + self.retry_at = retry_at + self.key = "overLimit" diff --git a/keystone/logic/types/tenant.py b/keystone/logic/types/tenant.py new file mode 100644 index 00000000..b161476b --- /dev/null +++ b/keystone/logic/types/tenant.py @@ -0,0 +1,119 @@ +# Copyright (c) 2010-2011 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 json +import keystone.logic.types.fault as fault +from lxml import etree +import string + + +class Tenant(object): + "Describes a tenant in the auth system" + + def __init__(self, tenant_id, description, enabled): + self.tenant_id = tenant_id + self.description = description + self.enabled = enabled and True or False + + @staticmethod + def from_xml(xml_str): + try: + dom = etree.Element("root") + dom.append(etree.fromstring(xml_str)) + root = dom.find("{http://docs.openstack.org/idm/api/v1.0}tenant") + if root == None: + raise fault.BadRequestFault("Expecting Tenant") + tenant_id = root.get("id") + enabled = root.get("enabled") + if enabled == None or enabled == "true" or enabled == "yes": + set_enabled = True + elif enabled == "false" or enabled == "no": + set_enabled = False + else: + raise fault.BadRequestFault("Bad enabled attribute!") + desc = root.find("{http://docs.openstack.org/idm/api/v1.0}" + "description") + if desc == None: + raise fault.BadRequestFault("Expecting Tenant Description") + return Tenant(tenant_id, desc.text, set_enabled) + except etree.LxmlError as e: + raise fault.BadRequestFault("Cannot parse Tenant", str(e)) + + @staticmethod + def from_json(json_str): + try: + obj = json.loads(json_str) + if not "tenant" in obj: + raise fault.BadRequestFault("Expecting tenant") + tenant = obj["tenant"] + if not "id" in tenant: + tenant_id = None + else: + tenant_id = tenant["id"] + set_enabled = True + if "enabled" in tenant: + set_enabled = tenant["enabled"] + if not isinstance(set_enabled, bool): + raise fault.BadRequestFault("Bad enabled attribute!") + if not "description" in tenant: + raise fault.BadRequestFault("Expecting Tenant Description") + description = tenant["description"] + return Tenant(tenant_id, description, set_enabled) + except (ValueError, TypeError) as e: + raise fault.BadRequestFault("Cannot parse Tenant", str(e)) + + def to_dom(self): + dom = etree.Element("tenant", + xmlns="http://docs.openstack.org/idm/api/v1.0", + enabled=string.lower(str(self.enabled))) + if self.tenant_id: + dom.set("id", self.tenant_id) + desc = etree.Element("description") + desc.text = self.description + dom.append(desc) + return dom + + def to_xml(self): + return etree.tostring(self.to_dom()) + + def to_dict(self): + tenant = {} + if self.tenant_id: + tenant["id"] = self.tenant_id + tenant["description"] = self.description + tenant["enabled"] = self.enabled + return {'tenant': tenant} + + def to_json(self): + return json.dumps(self.to_dict()) + + +class Tenants(object): + "A collection of tenants." + + def __init__(self, values, links): + self.values = values + self.links = links + + def to_xml(self): + dom = etree.Element("tenants", + xmlns="http://docs.openstack.org/idm/api/v1.0") + for t in self.values: + dom.append(t.to_dom()) + return etree.tostring(dom) + + def to_json(self): + values = [t.to_dict()["tenant"] for t in self.values] + return json.dumps({"tenants": {"values": values}}) diff --git a/keystone/middleware/__init__.py b/keystone/middleware/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone/middleware/remoteauth.py b/keystone/middleware/remoteauth.py new file mode 100644 index 00000000..5919abd6 --- /dev/null +++ b/keystone/middleware/remoteauth.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (c) 2010 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. + + +""" +Auth Middleware that handles auth for a service + +This module can be installed as a filter in front of your service to validate +that requests are coming from a trusted component that has handled +authenticating the call. If a call comes from an untrusted source, it will +redirect it back to be properly authenticated. This is done by sending our a +305 proxy redirect response with the URL for the auth service. + +The auth service settings are specified in the INI file (keystone.ini). The ini +file is passed in as the WSGI config file when starting the service. For this +proof of concept, the ini file is in echo/echo/echo.ini. + +In the current implementation use a basic auth password to verify that the +request is coming from a valid auth component or service + +Refer to: http://wiki.openstack.org/openstack-authn + + +HEADERS +------- +HTTP_ is a standard http header +HTTP_X is an extended http header + +> Coming in from initial call +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 cloud files +> Used for communication between components +www-authenticate : only used if this component is being used remotely +HTTP_AUTHORIZATION : basic auth password used to validate the connection + +> What we add to the request for use by the OpenStack service +HTTP_X_AUTHORIZATION: the client identity being passed in + + +""" + +from webob.exc import HTTPUseProxy, HTTPUnauthorized + + +class RemoteAuth(object): + + # app is the downstream WSGI component, usually the OpenStack service + # + # if app is not provided, the assumption is this filter is being run + # from a separate server. + + def __init__(self, app, conf): + # app is the next app in WSGI chain - eventually the OpenStack service + self.app = app + self.conf = conf + # where to redirect untrusted requests to go and auth + self.auth_location = conf.get('auth_location') + # secret that will tell us a request is coming from a trusted auth + # component + self.remote_auth_pass = conf.get('remote_auth_pass') + print 'Starting Remote Auth middleware' + + def __call__(self, env, start_response): + # Validate the request is trusted + # Authenticate the Auth component itself. + headers = [('www-authenticate', 'Basic realm="API Auth"')] + if 'HTTP_AUTHORIZATION' not in env: + return HTTPUnauthorized(headers=headers)(env, start_response) + else: + auth_type, encoded_creds = env['HTTP_AUTHORIZATION'].split(None, 1) + if encoded_creds != self.remote_auth_pass: + return HTTPUnauthorized(headers=headers)(env, start_response) + + # Make sure that the user has been authenticated by the Auth Service + if 'HTTP_X_AUTHORIZATION' not in env: + return HTTPUseProxy(location=self.auth_location)(env, + start_response) + + return self.app(env, start_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 RemoteAuth(app, conf) + return auth_filter diff --git a/keystone/samples b/keystone/samples new file mode 120000 index 00000000..d6685fde --- /dev/null +++ b/keystone/samples @@ -0,0 +1 @@ +../docs/guide/src/docbkx/samples \ No newline at end of file diff --git a/keystone/xsd b/keystone/xsd new file mode 120000 index 00000000..d63d56ef --- /dev/null +++ b/keystone/xsd @@ -0,0 +1 @@ +../docs/guide/src/docbkx/xsd \ No newline at end of file diff --git a/management/delgroup.py b/management/delgroup.py new file mode 100644 index 00000000..9ef2d733 --- /dev/null +++ b/management/delgroup.py @@ -0,0 +1,42 @@ +# Copyright (c) 2010-2011 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog group_id" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 1: + parser.error("Incorrect number of arguments") + else: + index = args[0] + + try: + o = db_api.group_get(index) + if o == None: + raise IndexError("Group %s not found", index) + else: + db_api.group_delete(index) + print 'Group', index, 'deleted.' + + except Exception, e: + print 'Error deleting group', index, str(e) + + +if __name__ == '__main__': + main() diff --git a/management/getgroup.py b/management/getgroup.py new file mode 100644 index 00000000..38a6b8f2 --- /dev/null +++ b/management/getgroup.py @@ -0,0 +1,40 @@ +# Copyright (c) 2010-2011 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog group_id" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 1: + parser.error("Incorrect number of arguments") + else: + index = args[0] + + try: + o = db_api.group_get(index) + if o == None: + raise IndexError("Group %s not found", index) + + print o.id, o.desc + except Exception, e: + print 'Error getting group', index, str(e) + + +if __name__ == '__main__': + main() diff --git a/management/getgroups.py b/management/getgroups.py new file mode 100644 index 00000000..501d948f --- /dev/null +++ b/management/getgroups.py @@ -0,0 +1,37 @@ +# Copyright (c) 2010-2011 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog " + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 0: + parser.error("Incorrect number of arguments") + else: + try: + u = db_api.group_get_all() + if u == None: + raise IndexError("Groups not found") + for row in u: + print row.id + except Exception, e: + print 'Error getting groups:', str(e) + +if __name__ == '__main__': + main() diff --git a/management/getgroupusers.py b/management/getgroupusers.py new file mode 100644 index 00000000..0fc7d231 --- /dev/null +++ b/management/getgroupusers.py @@ -0,0 +1,39 @@ +# Copyright (c) 2010-2011 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog group_id" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 1: + parser.error("Incorrect number of arguments") + else: + group_id = args[0] + + try: + g = db_api.group_users(group_id) + if g == None: + raise IndexError("Group users not found") + for row in g: + print row + except Exception, e: + print 'Error getting group users for group', group_id, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/getuser.py b/management/getuser.py new file mode 100644 index 00000000..036f10f4 --- /dev/null +++ b/management/getuser.py @@ -0,0 +1,37 @@ +# Copyright (c) 2010-2011 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog username" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 1: + parser.error("Incorrect number of arguments") + else: + username = args[0] + try: + u = db_api.user_get(username) + if u == None: + raise IndexError("User not found") + print u.id, u.email, u.enabled + except Exception, e: + print 'Error finding user', username, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/getusergroups.py b/management/getusergroups.py new file mode 100644 index 00000000..3fd7d3f9 --- /dev/null +++ b/management/getusergroups.py @@ -0,0 +1,38 @@ +# Copyright (c) 2010-2011 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog user_id" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 1: + parser.error("Incorrect number of arguments") + else: + username = args[0] + try: + g = db_api.user_groups(username) + if g == None: + raise IndexError("User groups not found") + for row in g: + print row + except Exception, e: + print 'Error getting user groups for user', user_id, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/getusers.py b/management/getusers.py new file mode 100644 index 00000000..18a5de56 --- /dev/null +++ b/management/getusers.py @@ -0,0 +1,38 @@ +# Copyright (c) 2010-2011 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog tenant_id" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 1: + parser.error("Incorrect number of arguments") + else: + tenant_id = args[0] + try: + u = db_api.user_get_by_tenant(tenant_id) + if u == None: + raise IndexError("Users not found") + for row in u: + print row + except Exception, e: + print 'Error getting users for tenant', tenant_id, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/groupadd.py b/management/groupadd.py new file mode 100644 index 00000000..adec58dc --- /dev/null +++ b/management/groupadd.py @@ -0,0 +1,40 @@ +# Copyright (c) 2010-2011 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 optparse +import keystone.db.sqlalchemy.api as db_api +from keystone.db.sqlalchemy import models + + +def main(): + usage = "usage: %prog group_id group_desc" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 2: + parser.error("Incorrect number of arguments") + else: + group_id = args[0] + group_desc = args[1] + try: + g = models.Group() + g.id = group_id + g.desc = group_desc + db_api.group_create(g) + print 'Group', g.id, 'created.' + except Exception, e: + print 'Error creating group', group_id, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/setuserlock.py b/management/setuserlock.py new file mode 100644 index 00000000..622d876b --- /dev/null +++ b/management/setuserlock.py @@ -0,0 +1,49 @@ +# Copyright (c) 2010-2011 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog username enabled" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 2: + parser.error("Incorrect number of arguments") + else: + username = args[0] + enabled = args[1].capitalize().strip() + + if enabled == 'True' or enabled == '1': + enabled = 1 + elif enabled == 'False' or enabled == '0': + enabled = 0 + else: + parser.error("Incorrect arguments value") + + try: + u = db_api.user_get(username) + if u == None: + raise IndexError("User not found") + else: + values = {'enabled': enabled} + db_api.user_update(username, values) + print 'User', u.id, 'updated. Enabled =', enabled + except Exception, e: + print 'Error updating user', username, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/setuserpswd.py b/management/setuserpswd.py new file mode 100644 index 00000000..6283bf73 --- /dev/null +++ b/management/setuserpswd.py @@ -0,0 +1,41 @@ +# Copyright (c) 2010-2011 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog username password" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 2: + parser.error("Incorrect number of arguments") + else: + username = args[0] + password = args[1] + try: + u = db_api.user_get(username) + if u == None: + raise IndexError("User not found") + else: + values = {'password': password} + db_api.user_update(username, values) + print 'User', u.id, 'updated.' + except Exception, e: + print 'Error updating user', username, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/updategroup.py b/management/updategroup.py new file mode 100644 index 00000000..598d38ef --- /dev/null +++ b/management/updategroup.py @@ -0,0 +1,42 @@ +# Copyright (c) 2010-2011 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 optparse +import keystone.db.sqlalchemy.api as db_api +from keystone.db.sqlalchemy import models + + +def main(): + usage = "usage: %prog group_id group_desc" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 2: + parser.error("Incorrect number of arguments") + else: + group = args[0] + desc = args[1] + try: + g = db_api.group_get(group) + if g == None: + raise IndexError("Group not found") + else: + values = {'desc': desc} + db_api.group_update(group, values) + print 'Group', g.id, 'updated.' + except Exception, e: + print 'Error updating user', group, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/useradd.py b/management/useradd.py new file mode 100644 index 00000000..fc3fc8b6 --- /dev/null +++ b/management/useradd.py @@ -0,0 +1,42 @@ +# Copyright (c) 2010-2011 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 optparse +import keystone.db.sqlalchemy.api as db_api +from keystone.db.sqlalchemy import models + + +def main(): + usage = "usage: %prog username password" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 2: + parser.error("Incorrect number of arguments") + else: + username = args[0] + password = args[1] + try: + u = models.User() + u.id = username + u.email = username + u.password = password + u.enabled = True + db_api.user_create(u) + print 'User', u.id, 'created.' + except Exception, e: + print 'Error creating user', username, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/userdel.py b/management/userdel.py new file mode 100644 index 00000000..928dfc55 --- /dev/null +++ b/management/userdel.py @@ -0,0 +1,39 @@ +# Copyright (c) 2010-2011 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog username" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 1: + parser.error("Incorrect number of arguments") + else: + username = args[0] + try: + u = db_api.user_get(username) + if u == None: + raise IndexError("User not found") + else: + db_api.user_delete(username) + print 'User', username, 'deleted.' + except Exception, e: + print 'Error deleting user', username, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/userupdate.py b/management/userupdate.py new file mode 100644 index 00000000..89174d04 --- /dev/null +++ b/management/userupdate.py @@ -0,0 +1,41 @@ +# Copyright (c) 2010-2011 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog username email" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 2: + parser.error("Incorrect number of arguments") + else: + username = args[0] + email = args[1] + try: + u = db_api.user_get(username) + if u == None: + raise IndexError("User not found") + else: + values = {'email': email} + db_api.user_update(username, values) + print 'User', u.id, 'updated.' + except Exception, e: + print 'Error updating user', username, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/pip-requires b/pip-requires new file mode 100644 index 00000000..1ef8484b --- /dev/null +++ b/pip-requires @@ -0,0 +1,9 @@ +bottle +eventlet +lxml +paste +pastedeploy +pastescript +pysqlite +sqlalchemy +webob diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..83ead0ca --- /dev/null +++ b/setup.py @@ -0,0 +1,42 @@ +#!/usr/bin/python +# Copyright (c) 2010-2011 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. + +from setuptools import setup, find_packages + +version = '1.0' + +setup( + name='keystone', + version=version, + description="Authentication service - proposed for OpenStack", + license='Apache License (2.0)', + classifiers=["Programming Language :: Python"], + keywords='identity auth authentication openstack', + author='OpenStack, LLC.', + author_email='openstack@lists.launchpad.net', + url='http://www.openstack.org', + include_package_data=True, + packages=find_packages(exclude=['test', 'bin']), + zip_safe=False, + install_requires=['setuptools'], + entry_points={ + 'paste.app_factory': ['main=identity:app_factory'], + 'paste.filter_factory': [ + 'papiauth=keystone:papiauth_factory', + 'tokenauth=keystone:tokenauth_factory', + ], + }, + ) diff --git a/test b/test deleted file mode 100644 index 257cc564..00000000 --- a/test +++ /dev/null @@ -1 +0,0 @@ -foo diff --git a/test/EchoSOAPUI.xml b/test/EchoSOAPUI.xml new file mode 100644 index 00000000..8a74eee0 --- /dev/null +++ b/test/EchoSOAPUI.xml @@ -0,0 +1,2 @@ + +http://localhost:8090X-Auth-TokenHEADERxs:string*/*application/xml200v1:echoapplication/json200<xml-fragment/>http://localhost:8090*/*application/xml200v1:echoapplication/json200http://localhost:8090*/*application/xml200v1:echoapplication/json200http://localhost:8090*/*application/xml200v1:echoapplication/json200http://localhost:8090*/*application/xml200v1:echoapplication/json200http://localhost:8090*/*application/xml200v1:echoapplication/json200http://localhost:8090 \ No newline at end of file diff --git a/test/IdentitySOAPUI.xml b/test/IdentitySOAPUI.xml new file mode 100644 index 00000000..3ed02484 --- /dev/null +++ b/test/IdentitySOAPUI.xml @@ -0,0 +1,1355 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]>file:/Users/jorgew/projects/keystone/keystone/identity.wadl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]>http://wadl.dev.java.net/2009/02file:/Users/jorgew/projects/keystone/keystone/xsd/api.xsd<schema elementFormDefault="qualified" attributeFormDefault="unqualified" targetNamespace="http://docs.openstack.org/idm/api/v1.0" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:idm="http://docs.openstack.org/idm/api/v1.0" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <include schemaLocation="token.xsd"/> + <include schemaLocation="tenant.xsd"/> + <include schemaLocation="fault.xsd"/> +</schema>http://www.w3.org/2001/XMLSchemafile:/Users/jorgew/projects/keystone/keystone/xsd/token.xsd + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]>http://www.w3.org/2001/XMLSchemafile:/Users/jorgew/projects/keystone/keystone/xsd/tenant.xsd + + + + + + + + + + + + + + + + + + + + + + + +]]>http://www.w3.org/2001/XMLSchemafile:/Users/jorgew/projects/keystone/keystone/xsd/atom/atom.xsd + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + See section 3.4 of the ATOM RFC + RFC4287 + + + + + + + TODO + + + + + + + TODO + + + + + + + TODO + + + + + + + TODO + + + + + + + TODO + + + + + + +]]>http://www.w3.org/2001/XMLSchemafile:/Users/jorgew/projects/keystone/keystone/xsd/atom/xml.xsd + + + +
+

About the XML namespace

+
+

This schema document describes the XML namespace, in a form + suitable for import by other schema documents.

+

+ See + http://www.w3.org/XML/1998/namespace.html + and + http://www.w3.org/TR/REC-xml + for information + about this namespace. +

+

Note that local names in this namespace are intended to be + defined only by the World Wide Web Consortium or its subgroups. + The names currently defined in this namespace are listed below. + They should not be used with conflicting semantics by any Working + Group, specification, or document instance.

+

+ See further below in this document for more information about + how to refer to this schema document from your own + XSD schema documents + and about + the + namespace-versioning policy governing this schema document + . +

+
+
+
+
+ + + +
+

lang (as an attribute name)

+

denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification.

+
+
+

Notes

+

Attempting to install the relevant ISO 2- and 3-letter + codes as the enumerated possible values is probably never + going to be a realistic possibility.

+

+ See BCP 47 at + http://www.rfc-editor.org/rfc/bcp/bcp47.txt + and the IANA language subtag registry at + http://www.iana.org/assignments/language-subtag-registry + for further information. +

+

The union allows for the 'un-declaration' of xml:lang with + the empty string.

+
+
+
+ + + + + + + + + +
+ + + +
+

space (as an attribute name)

+

denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification.

+
+
+
+ + + + + + +
+ + + +
+

base (as an attribute name)

+

denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification.

+

+ See + http://www.w3.org/TR/xmlbase/ + for information about this attribute. +

+
+
+
+
+ + + +
+

id (as an attribute name)

+

denotes an attribute whose value + should be interpreted as if declared to be of type ID. + This name is reserved by virtue of its definition in the + xml:id specification.

+

+ See + http://www.w3.org/TR/xml-id/ + for information about this attribute. +

+
+
+
+
+ + + + + + + + +
+

Father (in any context at all)

+
+

denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups:

+
+

In appreciation for his vision, leadership and + dedication the W3C XML Plenary on this 10th day of + February, 2000, reserves for Jon Bosak in perpetuity + the XML name "xml:Father".

+
+
+
+
+
+ + +
+

+ About this schema document +

+
+

+ This schema defines attributes and an attribute group suitable + for use by schemas wishing to allow + xml:base + , + xml:lang + , + xml:space + or + xml:id + attributes on elements they define. +

+

To enable this, such a schema must import this schema for + the XML namespace, e.g. as follows:

+
<schema . . .>
+           . . .
+           <import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+

or

+
<import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
+

Subsequently, qualified reference to any of the attributes or the + group defined below will have the desired effect, e.g.

+
<type . . .>
+           . . .
+           <attributeGroup ref="xml:specialAttrs"/>
+

will define a type which will schema-validate an instance element + with any of those attributes.

+
+
+
+
+ + +
+

+ Versioning policy for this schema document +

+
+

+ In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + http://www.w3.org/2009/01/xml.xsd + . +

+

+ At the date of issue it can also be found at + http://www.w3.org/2001/xml.xsd + . +

+

+ The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML + Schema itself, or with the XML namespace itself. In other words, + if the XML Schema or XML namespaces change, the version of this + document at + http://www.w3.org/2001/xml.xsd + will change accordingly; the version at + http://www.w3.org/2009/01/xml.xsd + will not change. +

+

Previous dated (and unchanging) versions of this schema + document are at:

+ +
+
+
+
+
]]>
http://www.w3.org/2001/XMLSchema
file:/Users/jorgew/projects/keystone/keystone/xsd/fault.xsd + + + + + + + + + + + + + + + + +

A human readable message that is appropriate for display + to the end user.

+
+
+
+ + + +

The optional <details> element may contain useful + information for tracking down errors (e.g a stack + trace). This information may or may not be appropriate + for display to an end user.

+
+
+
+ +
+ + + +

The HTTP status code associated with the current fault.

+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

An optional dateTime denoting when an operation should + be retried.

+
+
+
+
+
+
+]]>
http://www.w3.org/2001/XMLSchema
file:/Users/jorgew/projects/keystone/keystone/xsd/api-common.xsd + + + + + Open Stack Common API Schema Types 1.0 + + + + + +

This is the main index XML Schema document + for Common API Schema Types Version 1.0.

+
+
+ + + +

Types related to extensions.

+
+
+
+ + + +

Types related to API version details.

+
+
+
+
]]>
http://www.w3.org/2001/XMLSchema
file:/Users/jorgew/projects/keystone/keystone/xsd/extensions.xsd + + + + + + + + + + + + + + + + + + + + + + + + + +

There should be at least one atom link + with a describedby relation.

+
+
+
+
+ + + + + +]]>
http://www.w3.org/2001/XMLSchema
file:/Users/jorgew/projects/keystone/keystone/xsd/version.xsd + + + + + + + + + + + + + The VersionStatus type describes a service's operational status. + + + + + + + + + + + + + A version choice list outlines a collection of service version choices. + + + + + + + + + + + In version lists, every single version must + contain at least one self link. + + + + + + + + + + + When used as a root element, a version choice + must contain at least one describedby link. + + + + + + + + + + A version choice contains relevant information about an available service + that a user can then use to target a specific version of the service. Note + that both the descriptive media types and the atom link references are + not manditory and are offered as message enrichment elements rather + than message requirements. + + + + + + + + + + + The ID of a version choice represents the service version's unique + identifier. This ID is guaranteed to be unique only among the + service version choices outlined in the VersionChoiceList. + + + + + + + A version choice's status describes the current operational state of + the given service version. The operational status is captured in a + simple type enumeration called VersionStatus. + + + + + + + + A version choice's updated attribute describes + the time when the version was updated. The + time should be updated anytime + anything + in the + version has changed: documentation, + extensions, bug fixes. + + + + + + + + + + A MediaTypeList outlines a collection of valid media types for a given + service version. + + + + + + + + + + + + A MediaType describes what content types the service version understands. + + + + + + + + + The base of a given media type describes the simple MIME type + that then a more complicated media type can be derived from. These + types are basic and provide no namespace or version specific + data are are only provided as a convenience. Because of this the + base attribute is declared as optional. + + + + + + + + The type attribute of a MediaType describes the MIME specific + identifier of the media type in question. This identifier should include + a vendor namespace ( + See RFC 2048 + ) + as well as a version suffix. + + + + + + +]]>http://www.w3.org/2001/XMLSchema
http://localhost:8080aliasTEMPLATExs:stringapplication/xml200 203v1:extensionapplication/json200 203application/xml400v1:badRequestapplication/xml404v1:itemNotFoundapplication/xml500v1:idmFaultapplication/xml503v1:serviceUnavailableapplication/json400 404 500 503<xml-fragment/>http://localhost:8080application/xml200 203v1:extensionsapplication/json200 203application/xml400v1:badRequestapplication/xml500v1:idmFaultapplication/xml503v1:serviceUnavailableapplication/json400 500 503<xml-fragment/>http://localhost:8080X-Auth-TokenHEADERxs:stringtokenIdTEMPLATExs:stringbelongsToQUERYxs:stringapplication/xml200 203v1:authapplication/json200 203application/xml401v1:unauthorizedapplication/xml403v1:forbiddenapplication/xml403v1:userDisabledapplication/xml400v1:badRequestapplication/xml404v1:itemNotFoundapplication/xml500v1:idmFaultapplication/xml503v1:serviceUnavailableapplication/json400 401 403 404 500 503<xml-fragment/>http://localhost:8080 + + + +application/xml401v1:unauthorizedapplication/xml403v1:forbiddenapplication/xml400v1:badRequestapplication/xml404v1:itemNotFoundapplication/xml500v1:idmFaultapplication/xml503v1:serviceUnavailableapplication/json400 401 403 404 500 503<xml-fragment/>http://localhost:8080 + + +application/xmlv1:passwordCredentialsapplication/jsonapplication/xml200 +203v1:authapplication/json200 +203application/xml401v1:unauthorizedapplication/xml403v1:userDisabledapplication/xml400v1:badRequestapplication/xml500v1:idmFaultapplication/xml503v1:serviceUnavailableapplication/json401 +403 400 500 503<xml-fragment/>http://localhost:8080<passwordCredentials +password="secrete" username="joeuser" +xmlns="http://docs.openstack.org/idm/api/v1.0"/>X-Auth-TokenHEADERxs:stringtenantIdTEMPLATExs:stringapplication/xml200 +203v1:tenantapplication/json200 +203application/xml401v1:unauthorizedapplication/xml403v1:forbiddenapplication/xml400v1:badRequestapplication/xml404v1:itemNotFoundapplication/xml500v1:idmFaultapplication/xml503v1:serviceUnavailableapplication/json400 +401 403 404 500 503<xml-fragment/>http://localhost:8080 + + +application/xmlv1:tenantapplication/jsonapplication/xml200v1:tenantapplication/json200application/xml401v1:unauthorizedapplication/xml403v1:forbiddenapplication/xml404v1:itemNotFoundapplication/xml400v1:badRequestapplication/xml500v1:idmFaultapplication/xml503v1:serviceUnavailableapplication/json401 +403 404 400 500 503<xml-fragment/>http://localhost:8080<v1:tenant enabled="true" xmlns:v1="http://docs.openstack.org/idm/api/v1.0"> + <v1:description>New Description</v1:description> +</v1:tenant> + + +application/xml401v1:unauthorizedapplication/xml403v1:forbiddenapplication/xml400v1:badRequestapplication/xml404v1:itemNotFoundapplication/xml500v1:idmFaultapplication/xml503v1:serviceUnavailableapplication/json400 +401 403 404 500 503<entry key="Accept" value="application/xml" xmlns="http://eviware.com/soapui/config"/>http://localhost:8080 + + +markerQUERYxs:stringlimitQUERYxs:intapplication/xml200 +203v1:tenantsapplication/json200 +203application/xml401v1:unauthorizedapplication/xml403v1:forbiddenapplication/xml400v1:badRequestapplication/xml404v1:itemNotFoundapplication/xml500v1:idmFaultapplication/xml503v1:serviceUnavailableapplication/json400 +401 403 404 500 503<xml-fragment/>http://localhost:8080application/xmlv1:tenantapplication/jsonapplication/xml201v1:tenantapplication/json201application/xml401v1:unauthorizedapplication/xml403v1:forbiddenapplication/xml +409v1:tenantConflictapplication/xml +400v1:badRequestapplication/xml500v1:idmFaultapplication/xml503v1:serviceUnavailableapplication/json401 +403 400 409 500 503<xml-fragment/>http://localhost:8080<v1:tenant +enabled="true" id="my_new_tenant" +xmlns:v1="http://docs.openstack.org/idm/api/v1.0"><v1:description>This +is a description of my tenant. Thank you very +much.</v1:description></v1:tenant>application/xml200 +203v1:versionapplication/json200 +203application/xml400v1:badRequestapplication/xml500v1:idmFaultapplication/xml503v1:serviceUnavailableapplication/json400 +500 503<xml-fragment/>http://localhost:8080
SEQUENTIAL<xml-fragment/>http://localhost:8080authfalsefalsetokenfalsefalseuserfalsefalse + + + +<xml-fragment/>http://localhost:8080unauthorizedfalsefalse401falsefalse + + + +<xml-fragment/>http://localhost:8080unauthorizedfalsefalse401falsefalse + + + +<xml-fragment/>http://localhost:8080unauthorizedfalsefalse401falsefalse + + + +<xml-fragment/>http://localhost:8080forbiddenfalsefalse403falsefalse + + + +<xml-fragment/>http://localhost:8080userDisabledfalsefalse403falsefalse + + + +<xml-fragment/>http://localhost:8080authfalsefalsetokenfalsefalseuserfalsefalse + + + +<xml-fragment/>http://localhost:8080authfalsefalsetokenfalsefalseuserfalsefalse + + +<xml-fragment/>http://localhost:8080itemNotFoundfalsefalse404falsefalse + + + +<xml-fragment/>http://localhost:8080itemNotFoundfalsefalse404falsefalse + + +<xml-fragment/>http://localhost:8080itemNotFoundfalsefalse404falsefalse + + +<xml-fragment/>http://localhost:8080<passwordCredentials password="P@ssword1" username="testuser" xmlns="http://docs.openstack.org/idm/api/v1.0"/>401falsefalseunauthorizedfalsefalse<xml-fragment/>http://localhost:8080<passwordCredentials password="1234" username="disabled" xmlns="http://docs.openstack.org/idm/api/v1.0"/>403falsefalseuserDisabledfalsefalse<xml-fragment/>http://localhost:8080<passwordCredentials password="123774" username="joeuser" xmlns="http://docs.openstack.org/idm/api/v1.0"/>401falsefalseunauthorizedfalsefalse<xml-fragment/>http://localhost:8080<passwordCredentials password="secrete" username="admin" xmlns="http://docs.openstack.org/idm/api/v1.0"/>userfalsefalsetokenfalsefalseAdminfalsefalsedeclare namespace auth='http://docs.openstack.org/idm/api/v1.0'; +/auth:auth/auth:user/auth:groups/auth:group/@id='Admin'truefalsefalse<xml-fragment/>http://localhost:8080<passwordCredentials password="secrete" username="joeuser" xmlns="http://docs.openstack.org/idm/api/v1.0"/>userfalsefalsetokenfalsefalseAdminfalsefalsedeclare namespace auth='http://docs.openstack.org/idm/api/v1.0'; +/auth:auth/auth:user/auth:groups/auth:group/@id='Admin'falsefalsefalse<xml-fragment/>http://localhost:8080{ + "passwordCredentials" : { + "username" : "testuser", + "password" : "P@ssword1" + } +}401falsefalseunauthorizedfalsefalse<xml-fragment/>http://localhost:8080{ + "passwordCredentials" : { + "username" : "disabled", + "password" : "1234" + } +}403falsefalseuserDisabledfalsefalse<xml-fragment/>http://localhost:8080{ + "passwordCredentials" : { + "username" : "joeuser", + "password" : "123774" + } +}401falsefalseunauthorizedfalsefalse<xml-fragment/>http://localhost:8080{ + "passwordCredentials" : { + "username" : "admin", + "password" : "secrete" + } +}userfalsefalsetokenfalsefalseAdminfalsefalse<xml-fragment/>http://localhost:8080{ + "passwordCredentials" : { + "username" : "joeuser", + "password" : "secrete" + } +}userfalsefalsetokenfalsefalseAdminfalsefalse<entry key="Accept" value="application/xml" xmlns="http://eviware.com/soapui/config"/>http://localhost:8080404falsefalseitemNotFoundfalsefalse + + +<xml-fragment/>http://localhost:8080<passwordCredentials password="secrete" username="joeuser" xmlns="http://docs.openstack.org/idm/api/v1.0"/>userfalsefalsetokenfalsefalseAdminfalsefalsedeclare namespace auth='http://docs.openstack.org/idm/api/v1.0'; +/auth:auth/auth:user/auth:groups/auth:group/@id='Admin'falsefalsefalsedeclare namespace auth='http://docs.openstack.org/idm/api/v1.0'; +/auth:auth/auth:token/@id887665443383838falsefalse<entry key="Accept" value="application/xml" xmlns="http://eviware.com/soapui/config"/>http://localhost:8080assert(context.response==null) + + +<xml-fragment/>http://localhost:8080itemNotFoundfalsefalse404falsefalse + + +<xml-fragment/>http://localhost:8080<passwordCredentials password="secrete" username="joeuser" xmlns="http://docs.openstack.org/idm/api/v1.0"/>userfalsefalsetokenfalsefalseAdminfalsefalsedeclare namespace auth='http://docs.openstack.org/idm/api/v1.0'; +/auth:auth/auth:user/auth:groups/auth:group/@id='Admin'falsefalsefalsedeclare namespace auth='http://docs.openstack.org/idm/api/v1.0'; +/auth:auth/auth:token/@id="887665443383838"falsefalsefalse<xml-fragment/>http://localhost:8080<v1:tenant enabled="true" id="my_new_tenant" xmlns:v1="http://docs.openstack.org/idm/api/v1.0"><v1:description>This is a description of my tenant. Thank you very much.</v1:description></v1:tenant>declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@enabled = "true" and /ns1:tenant/@id="my_new_tenant" and /ns1:tenant/ns1:description = "This is a description of my tenant. Thank you very much."truefalsefalse<xml-fragment/>http://localhost:8080<v1:tenant enabled="true" id="my_new_tenant" xmlns:v1="http://docs.openstack.org/idm/api/v1.0"><v1:description>This is a description of my tenant. Thank you very much.</v1:description></v1:tenant>tenantConflictfalsefalse409falsefalse<xml-fragment/>http://localhost:8080<v1:tenant enabled="false" id="mt2" xmlns:v1="http://docs.openstack.org/idm/api/v1.0"><v1:description>New Disabled Tenant</v1:description></v1:tenant>declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@enabled = "false" and /ns1:tenant/@id="mt2" and /ns1:tenant/ns1:description = "New Disabled Tenant"truefalsefalse<xml-fragment/>http://localhost:8080<v1:tenant id="mt3" xmlns:v1="http://docs.openstack.org/idm/api/v1.0"><v1:description>New Tenant 3</v1:description></v1:tenant>declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@id="mt3" and /ns1:tenant/ns1:description = "New Tenant 3"truefalsefalse<xml-fragment/>http://localhost:8080<v1:tenant enabled="true" xmlns:v1="http://docs.openstack.org/idm/api/v1.0"><v1:description>New Tenant No ID</v1:description></v1:tenant>400falsefalsebadRequestfalsefalse<xml-fragment/>http://localhost:8080<v1:tenant enabled="true" id="my_new_tenant" xmlns:v1="http://docs.openstack.org/idm/api/v1.0"></v1:tenant>400falsefalsebadRequestfalsefalse<xml-fragment/>http://localhost:8080{"tenant": + { + "id": "JGroup", + "description": "A description ...", + "enabled": true + } +} +declare namespace ns1='http://localhost/v1.0/tenants'; +ns1:Response/ns1:tenant/ns1:id="JGroup" and ns1:Response/ns1:tenant/ns1:enabled="true" and ns1:Response/ns1:tenant/ns1:description="A description ..."truefalsefalse<xml-fragment/>http://localhost:8080{"tenant": + { + "id": "JGroup", + "description": "A description ...", + "enabled": true + } +}tenantConflictfalsefalse409falsefalse<xml-fragment/>http://localhost:8080{"tenant": + { + "id": "JGroup33", + "description": "A description...", + "enabled": false + } +}declare namespace ns1='http://localhost/v1.0/tenants'; +ns1:Response/ns1:tenant/ns1:id = "JGroup33" and ns1:Response/ns1:tenant/ns1:enabled="false" and ns1:Response/ns1:tenant/ns1:description="A description..."truefalsefalse<xml-fragment/>http://localhost:8080{"tenant": + { + "id": "JGroup65", + "description": "A description..." + } +}declare namespace ns1='http://localhost/v1.0/tenants'; +ns1:Response/ns1:tenant/ns1:id = "JGroup65" and ns1:Response/ns1:tenant/ns1:description = "A description..."truefalsefalse<xml-fragment/>http://localhost:8080{"tenant": + { + "description": "A description...", + "enabled" : true + } +}400falsefalsebadRequestfalsefalse<xml-fragment/>http://localhost:8080{"tenant": + { + "id": "JGroup95", + "enabled": true + } +}400falsefalsebadRequestfalsefalse<xml-fragment/>http://localhost:8080{"tenant": + { + "id": "JGroup95", + "description" : "A description...", + "enabled": "true" + } +}400falsefalsebadRequestfalsefalse<xml-fragment/>http://localhost:8080declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +count(//ns1:tenant)8falsefalse<xml-fragment/>http://localhost:8080declare namespace ns1='http://localhost/v1.0/tenants'; +count(//ns1:e)8falsefalse<xml-fragment/>http://localhost:8080declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +ns1:tenant/@id1234falsefalsedeclare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@enabled and /ns1:tenant/ns1:descriptiontruefalsefalse + + +<xml-fragment/>http://localhost:8080declare namespace ns1='http://localhost/v1.0/tenants/1234'; +ns1:Response/ns1:tenant/ns1:id1234falsefalsedeclare namespace ns1='http://localhost/v1.0/tenants/1234'; +/ns1:Response/ns1:tenant/ns1:enabled and /ns1:Response/ns1:tenant/ns1:descriptiontruefalsefalse + + +<xml-fragment/>http://localhost:8080404falsefalseitemNotFoundfalsefalse + + +<xml-fragment/>http://localhost:8080<v1:tenant enabled="true" id="to_delete" + xmlns:v1="http://docs.openstack.org/idm/api/v1.0"> + <v1:description>To Be Deleted</v1:description> +</v1:tenant>declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@enabled = "true" and /ns1:tenant/@id="to_delete" and /ns1:tenant/ns1:description = "To Be Deleted"truefalsefalse<xml-fragment/>http://localhost:8080declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +ns1:tenant/@idto_deletefalsefalsedeclare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@enabled and /ns1:tenant/ns1:descriptiontruefalsefalse + + +<entry key="Accept" value="application/xml" xmlns="http://eviware.com/soapui/config"/>http://localhost:8080assert(context.response == null) + + +<xml-fragment/>http://localhost:8080404falsefalseitemNotFoundfalsefalse + + +<entry key="Accept" value="application/xml" xmlns="http://eviware.com/soapui/config"/>http://localhost:8080forbiddenfalsefalse403falsefalse + + +<entry key="Accept" value="application/xml" xmlns="http://eviware.com/soapui/config"/>http://localhost:8080forbiddenfalsefalse403falsefalse + + +<xml-fragment/>http://localhost:8080<v1:tenant enabled="true" id="to_update" xmlns:v1="http://docs.openstack.org/idm/api/v1.0"> + <v1:description>ToUpdate</v1:description> +</v1:tenant>declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@enabled = "true" and /ns1:tenant/@id="to_update" and /ns1:tenant/ns1:description = "ToUpdate"truefalsefalse<xml-fragment/>http://localhost:8080declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@id="to_update" and /ns1:tenant/@enabled = "true" and /ns1:tenant/@id="to_update" and /ns1:tenant/ns1:description = "ToUpdate"truefalsefalse + + +<xml-fragment/>http://localhost:8080<v1:tenant enabled="true" xmlns:v1="http://docs.openstack.org/idm/api/v1.0"> + <v1:description>ToUpdate2</v1:description> +</v1:tenant>declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@id="to_update" and /ns1:tenant/@enabled = "true" and /ns1:tenant/ns1:description = "ToUpdate2"truefalsefalse + + +<xml-fragment/>http://localhost:8080declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@id="to_update" and /ns1:tenant/@enabled = "true" and /ns1:tenant/@id="to_update" and /ns1:tenant/ns1:description = "ToUpdate2"truefalsefalse + + +<xml-fragment/>http://localhost:8080<v1:tenant enabled="false" xmlns:v1="http://docs.openstack.org/idm/api/v1.0"> + <v1:description>ToUpdate2</v1:description> +</v1:tenant>declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@id="to_update" and /ns1:tenant/@enabled = "false" and /ns1:tenant/ns1:description = "ToUpdate2"truefalsefalse + + +<xml-fragment/>http://localhost:8080declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@id="to_update" and /ns1:tenant/@enabled = "false" and /ns1:tenant/@id="to_update" and /ns1:tenant/ns1:description = "ToUpdate2"truefalsefalse + + +<xml-fragment/>http://localhost:8080<v1:tenant id="boogabooga" enabled="false" xmlns:v1="http://docs.openstack.org/idm/api/v1.0"> + <v1:description>ToUpdate2</v1:description> +</v1:tenant>declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@id="to_update" and /ns1:tenant/@enabled = "false" and /ns1:tenant/ns1:description = "ToUpdate2"truefalsefalse + + +<xml-fragment/>http://localhost:8080declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@id="to_update" and /ns1:tenant/@enabled = "false" and /ns1:tenant/@id="to_update" and /ns1:tenant/ns1:description = "ToUpdate2"truefalsefalse + + +<xml-fragment/>http://localhost:8080<v1:tenant enabled="true" xmlns:v1="http://docs.openstack.org/idm/api/v1.0"> + <v1:description>ToUpdate3</v1:description> +</v1:tenant>declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@id="to_update" and /ns1:tenant/@enabled = "true" and /ns1:tenant/ns1:description = "ToUpdate3"truefalsefalse + + +<xml-fragment/>http://localhost:8080declare namespace ns1='http://docs.openstack.org/idm/api/v1.0'; +/ns1:tenant/@id="to_update" and /ns1:tenant/@enabled = "true" and /ns1:tenant/@id="to_update" and /ns1:tenant/ns1:description = "ToUpdate3"truefalsefalse + + +<entry key="Accept" value="application/xml" xmlns="http://eviware.com/soapui/config"/>http://localhost:8080assert(context.response == null) + + +<xml-fragment/>http://localhost:8080declare namespace ns1='http://docs.openstack.org/common/api/v1.0'; +count(/ns1:extensions)1falsefalse<xml-fragment/>http://localhost:8080declare namespace ns1='http://localhost/v1.0/extensions'; +count(//ns1:extensions)1falsefalse<xml-fragment/>http://localhost:8080404falsefalseitemNotFoundfalsefalse<xml-fragment/>http://localhost:8080404falsefalseitemNotFoundfalsefalse<xml-fragment/>http://localhost:8080declare namespace ns1='http://docs.openstack.org/common/api/v1.0'; +count(//ns1:version)1falsefalse<xml-fragment/>http://localhost:8080declare namespace ns1='http://localhost/v1.0'; +count(//ns1:version)1falsefalse
\ No newline at end of file diff --git a/test/kill.sql b/test/kill.sql new file mode 100644 index 00000000..66e3733f --- /dev/null +++ b/test/kill.sql @@ -0,0 +1,10 @@ +-- +-- Clean up the DB +-- + +delete from users; +delete from tenants; +delete from groups; +delete from user_group_association; +delete from user_tenant_association; +delete from token; diff --git a/test/test_setup.sql b/test/test_setup.sql new file mode 100644 index 00000000..a15f6d8e --- /dev/null +++ b/test/test_setup.sql @@ -0,0 +1,71 @@ +-- +-- Test Setup +-- + +-- Users + +insert into users (id, password, email, enabled) values + ("joeuser", "secrete", "joe@rackspace.com", 1); + +insert into users (id, password, email, enabled) values + ("admin", "secrete", "admin@rackspace.com", 1); + +insert into users (id, password, email, enabled) values + ("disabled", "secrete", "disable@rackspace.com", 0); + +-- Tenants + +insert into tenants (id, "desc", enabled) values + ("1234", "This is a tenant", 1); + +insert into tenants (id, "desc", enabled) values + ("0000", "This one is disabled", 0); + +-- Groups + +insert into groups (id, "desc", tenant_id) values + ("Admin", "Andmin users", "1234"); + +insert into groups (id, "desc", tenant_id) values + ("Default", "Standard users", "1234"); + +insert into groups (id, "desc", tenant_id) values + ("Empty", "Group on disabled tenant", "0000"); + + +-- User Group Associations + +insert into user_group_association values + ("joeuser", "Default"); + +insert into user_group_association values + ("disabled", "Default"); + +insert into user_group_association values + ("admin", "Admin"); + +-- User Tenant Associations + +insert into user_tenant_association values + ("joeuser", "1234"); + +insert into user_tenant_association values + ("disabled", "1234"); + +insert into user_tenant_association values + ("admin", "1234"); + +-- Token + +insert into token values + ("887665443383838", "joeuser", "1234", datetime("2012-02-05T00:00")); + +insert into token values + ("999888777666", "admin", "1234", datetime("2015-02-05T00:00")); + +insert into token values + ("000999", "admin", "1234", datetime("2010-02-05T00:00")); + +insert into token values + ("999888777", "disabled", "1234", datetime("2015-02-05T00:00")); + diff --git a/test/unit/test_identity.py b/test/unit/test_identity.py new file mode 100644 index 00000000..95a91afc --- /dev/null +++ b/test/unit/test_identity.py @@ -0,0 +1,966 @@ +import httplib2 +import json +from lxml import etree +import unittest +from webtest import TestApp + +URL = 'http://localhost:8080/v1.0/' + + +def get_token(user, pswd, kind=''): + h = httplib2.Http(".cache") + url = '%stoken' % URL + body = {"passwordCredentials": {"username": user, + "password": pswd}} + resp, content = h.request(url, "POST", body=json.dumps(body), + headers={"Content-Type": "application/json"}) + content = json.loads(content) + token = str(content['auth']['token']['id']) + if kind == 'token': + return token + else: + return (resp, content) + + +def get_token_xml(user, pswd, type=''): + h = httplib2.Http(".cache") + url = '%stoken' % URL + body = ' \ + ' % (pswd, user) + resp, content = h.request(url, "POST", body=body,\ + headers={"Content-Type": "application/xml", + "ACCEPT": "application/xml"}) + dom = etree.fromstring(content) + root = dom.find("{http://docs.openstack.org/idm/api/v1.0}token") + token_root = root.attrib + token = str(token_root['id']) + if type == 'token': + return token + else: + return (resp, content) + + +def delete_token(token, auth_token): + h = httplib2.Http(".cache") + url = '%stoken/%s' % (URL, token) + resp, content = h.request(url, "DELETE", body='', \ + headers={"Content-Type": "application/json", \ + "X-Auth-Token": auth_token}) + return (resp, content) + + +def delete_token_xml(token, auth_token): + h = httplib2.Http(".cache") + url = '%stoken/%s' % (URL, token) + resp, content = h.request(url, "DELETE", body='',\ + headers={"Content-Type": "application/xml", \ + "X-Auth-Token": auth_token, + "ACCEPT": "application/xml"}) + return (resp, content) + + +def create_tenant(tenantid, auth_token): + h = httplib2.Http(".cache") + + url = '%stenants' % (URL) + body = {"tenant": {"id": tenantid, + "description": "A description ...", + "enabled": True}} + resp, content = h.request(url, "POST", body=json.dumps(body), + headers={"Content-Type": "application/json", + "X-Auth-Token": auth_token}) + return (resp, content) + + +def create_tenant_xml(tenantid, auth_token): + h = httplib2.Http(".cache") + url = '%stenants' % (URL) + body = ' \ + \ + A description... \ + ' % tenantid + resp, content = h.request(url, "POST", body=body,\ + headers={"Content-Type": "application/xml",\ + "X-Auth-Token": auth_token, + "ACCEPT": "application/xml"}) + return (resp, content) + + +def delete_tenant(tenantid, auth_token): + h = httplib2.Http(".cache") + url = '%stenants/%s' % (URL, tenantid) + resp, content = h.request(url, "DELETE", body='{}',\ + headers={"Content-Type": "application/json",\ + "X-Auth-Token": auth_token}) + return (resp, content) + + +def delete_tenant_xml(tenantid, auth_token): + h = httplib2.Http(".cache") + url = '%stenants/%s' % (URL, tenantid) + resp, content = h.request(url, "DELETE", body='',\ + headers={"Content-Type": "application/xml",\ + "X-Auth-Token": auth_token, + "ACCEPT": "application/xml"}) + return (resp, content) + + +def get_tenant(): + return '1234' + + +def get_user(): + return '1234' + + +def get_userdisabled(): + return '1234' + + +def get_auth_token(): + return '999888777666' + + +def get_exp_auth_token(): + return '000999' + + +def get_disabled_token(): + return '999888777' + + +class identity_test(unittest.TestCase): + + #Given _a_ to make inherited test cases in an order. + #here to call below method will call as last test case + + def test_a_get_version(self): + h = httplib2.Http(".cache") + url = URL + resp, content = h.request(url, "GET", body="", + headers={"Content-Type": "application/json"}) + self.assertEqual(200, int(resp['status'])) + self.assertEqual('application/json', resp['content-type']) + + def test_a_get_version(self): + h = httplib2.Http(".cache") + url = URL + resp, content = h.request(url, "GET", body="", + headers={"Content-Type": "application/xml", + "ACCEPT": "application/xml"}) + self.assertEqual(200, int(resp['status'])) + self.assertEqual('application/xml', resp['content-type']) + + +class authorize_test(identity_test): + + def setUp(self): + self.token = get_token('joeuser', 'secrete', 'token') + self.tenant = get_tenant() + self.user = get_user() + self.userdisabled = get_userdisabled() + self.auth_token = get_auth_token() + self.exp_auth_token = get_exp_auth_token() + self.disabled_token = get_disabled_token() + + def tearDown(self): + delete_token(self.token, self.auth_token) + + def test_a_authorize(self): + resp, content = get_token('joeuser', 'secrete') + self.assertEqual(200, int(resp['status'])) + self.assertEqual('application/json', resp['content-type']) + + def test_a_authorize_xml(self): + resp, content = get_token_xml('joeuser', 'secrete') + self.assertEqual(200, int(resp['status'])) + self.assertEqual('application/xml', resp['content-type']) + + def test_a_authorize_user_disaabled(self): + h = httplib2.Http(".cache") + url = '%stoken' % URL + body = {"passwordCredentials": {"username": "disabled", + "password": "secrete"}} + resp, content = h.request(url, "POST", body=json.dumps(body), + headers={"Content-Type": "application/json"}) + content = json.loads(content) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(403, int(resp['status'])) + + def test_a_authorize_user_disaabled_xml(self): + h = httplib2.Http(".cache") + url = '%stoken' % URL + + body = ' \ + ' + resp, content = h.request(url, "POST", body=body,\ + headers={"Content-Type": "application/xml", + "ACCEPT": "application/xml"}) + content = etree.fromstring(content) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(403, int(resp['status'])) + + def test_a_authorize_user_wrong(self): + h = httplib2.Http(".cache") + url = '%stoken' % URL + body = {"passwordCredentials": {"username-w": "disabled", + "password": "secrete"}} + resp, content = h.request(url, "POST", body=json.dumps(body), + headers={"Content-Type": "application/json"}) + content = json.loads(content) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(400, int(resp['status'])) + + def test_a_authorize_user_wrong_xml(self): + h = httplib2.Http(".cache") + url = '%stoken' % URL + body = ' \ + ' + resp, content = h.request(url, "POST", body=body,\ + headers={"Content-Type": "application/xml", + "ACCEPT": "application/xml"}) + content = etree.fromstring(content) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(400, int(resp['status'])) + + +class validate_token(authorize_test): + + def test_validate_token_true(self): + h = httplib2.Http(".cache") + + url = '%stoken/%s?belongsTo=%s' % (URL, self.token, self.tenant) + resp, content = h.request(url, "GET", body='',\ + headers={"Content-Type": "application/json", \ + "X-Auth-Token": self.auth_token}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(200, int(resp['status'])) + self.assertEqual('application/json', resp['content-type']) + + def test_validate_token_true_xml(self): + h = httplib2.Http(".cache") + url = '%stoken/%s?belongsTo=%s' % (URL, self.token, self.tenant) + resp, content = h.request(url, "GET", body='',\ + headers={"Content-Type": "application/xml", \ + "X-Auth-Token": self.auth_token, + "ACCEPT": "application/xml"}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(200, int(resp['status'])) + self.assertEqual('application/xml', resp['content-type']) + + def test_validate_token_expired(self): + h = httplib2.Http(".cache") + url = '%stoken/%s?belongsTo=%s' % (URL, self.exp_auth_token, \ + self.tenant) + resp, content = h.request(url, "GET", body='',\ + headers={"Content-Type": "application/json", \ + "X-Auth-Token": self.exp_auth_token}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(401, int(resp['status'])) + self.assertEqual('application/json', resp['content-type']) + + def test_validate_token_expired_xml(self): + h = httplib2.Http(".cache") + + url = '%stoken/%s?belongsTo=%s' % (URL, self.exp_auth_token, \ + self.tenant) + resp, content = h.request(url, "GET", body='',\ + headers={"Content-Type": "application/xml", \ + "X-Auth-Token": self.exp_auth_token, + "ACCEPT": "application/xml"}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(401, int(resp['status'])) + self.assertEqual('application/xml', resp['content-type']) + + def test_validate_token_invalid(self): + h = httplib2.Http(".cache") + url = '%stoken/%s?belongsTo=%s' % (URL, 'NonExistingToken', \ + self.tenant) + resp, content = h.request(url, "GET", body='',\ + headers={"Content-Type": "application/json", \ + "X-Auth-Token": self.auth_token}) + + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(404, int(resp['status'])) + self.assertEqual('application/json', resp['content-type']) + + def test_validate_token_invalid_xml(self): + h = httplib2.Http(".cache") + url = '%stoken/%s?belongsTo=%s' % (URL, 'NonExistingToken', \ + self.tenant) + resp, content = h.request(url, "GET", body='',\ + headers={"Content-Type": "application/json", \ + "X-Auth-Token": self.auth_token}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(404, int(resp['status'])) + self.assertEqual('application/json', resp['content-type']) + + +class tenant_test(unittest.TestCase): + + def setUp(self): + self.token = get_token('joeuser', 'secrete', 'token') + self.tenant = get_tenant() + self.user = get_user() + self.userdisabled = get_userdisabled() + self.auth_token = get_auth_token() + self.exp_auth_token = get_exp_auth_token() + self.disabled_token = get_disabled_token() + + def tearDown(self): + resp, content = delete_tenant(self.tenant, self.auth_token) + + +class create_tenant_test(tenant_test): + + def test_tenant_create(self): + resp, content = delete_tenant('test_tenant', str(self.auth_token)) + resp, content = create_tenant('test_tenant', str(self.auth_token)) + self.tenant = 'test_tenant' + + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + + if int(resp['status']) not in (200, 201): + + self.fail('Failed due to %d' % int(resp['status'])) + + def test_tenant_create_xml(self): + resp, content = delete_tenant_xml('test_tenant', str(self.auth_token)) + resp, content = create_tenant_xml('test_tenant', str(self.auth_token)) + self.tenant = 'test_tenant' + content = etree.fromstring(content) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + + if int(resp['status']) not in (200, 201): + + self.fail('Failed due to %d' % int(resp['status'])) + + def test_tenant_create_again(self): + + resp, content = create_tenant("test_tenant", str(self.auth_token)) + resp, content = create_tenant("test_tenant", str(self.auth_token)) + if int(resp['status']) == 200: + self.tenant = content['tenant']['id'] + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(409, int(resp['status'])) + if int(resp['status']) == 200: + self.tenant = content['tenant']['id'] + + def test_tenant_create_again_xml(self): + + resp, content = create_tenant_xml("test_tenant", str(self.auth_token)) + resp, content = create_tenant_xml("test_tenant", str(self.auth_token)) + content = etree.fromstring(content) + if int(resp['status']) == 200: + self.tenant = content.get("id") + + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(409, int(resp['status'])) + if int(resp['status']) == 200: + self.tenant = content.get("id") + + def test_tenant_create_forbidden_token(self): + h = httplib2.Http(".cache") + resp, content = create_tenant("test_tenant", str(self.auth_token)) + if int(resp['status']) == 200: + self.tenant = content['tenant']['id'] + + url = '%stenants' % (URL) + body = {"tenant": {"id": self.tenant, + "description": "A description ...", + "enabled": True}} + resp, content = h.request(url, "POST", body=json.dumps(body), + headers={"Content-Type": "application/json", + "X-Auth-Token": self.token}) + + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(403, int(resp['status'])) + + def test_tenant_create_forbidden_token_xml(self): + h = httplib2.Http(".cache") + resp, content = create_tenant_xml("test_tenant", str(self.auth_token)) + content = etree.fromstring(content) + if int(resp['status']) == 200: + self.tenant = content.get('id') + + url = '%stenants' % (URL) + body = ' \ + \ + A description... \ + ' % self.tenant + resp, content = h.request(url, "POST", body=body,\ + headers={"Content-Type": "application/xml",\ + "X-Auth-Token": self.token, + "ACCEPT": "application/xml"}) + + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(403, int(resp['status'])) + + def test_tenant_create_expired_token(self): + h = httplib2.Http(".cache") + resp, content = create_tenant("test_tenant", str(self.auth_token)) + if int(resp['status']) == 200: + self.tenant = content['tenant']['id'] + + url = '%stenants' % (URL) + body = {"tenant": {"id": self.tenant, + "description": "A description ...", + "enabled": True}} + resp, content = h.request(url, "POST", body=json.dumps(body), + headers={"Content-Type": "application/json", + "X-Auth-Token": self.exp_auth_token}) + + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(401, int(resp['status'])) + + def test_tenant_create_expired_token_xml(self): + h = httplib2.Http(".cache") + resp, content = create_tenant_xml("test_tenant", str(self.auth_token)) + content = etree.fromstring(content) + if int(resp['status']) == 200: + self.tenant = content.get('id') + + url = '%stenants' % (URL) + body = ' \ + \ + A description... \ + ' % self.tenant + + resp, content = h.request(url, "POST", body=body,\ + headers={"Content-Type": "application/xml",\ + "X-Auth-Token": self.exp_auth_token, + "ACCEPT": "application/xml"}) + + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(401, int(resp['status'])) + + def test_tenant_create_missing_token(self): + h = httplib2.Http(".cache") + resp, content = create_tenant("test_tenant", str(self.auth_token)) + if int(resp['status']) == 200: + self.tenant = content['tenant']['id'] + + url = '%stenants' % (URL) + body = {"tenant": {"id": self.tenant, + "description": "A description ...", + "enabled": True}} + resp, content = h.request(url, "POST", body=json.dumps(body), + headers={"Content-Type": "application/json"}) + + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(401, int(resp['status'])) + + def test_tenant_create_missing_token_xml(self): + h = httplib2.Http(".cache") + resp, content = create_tenant_xml("test_tenant", str(self.auth_token)) + content = etree.fromstring(content) + if int(resp['status']) == 200: + self.tenant = content.get('id') + + url = '%stenants' % (URL) + + body = ' \ + \ + A description... \ + ' % self.tenant + resp, content = h.request(url, "POST", body=body,\ + headers={"Content-Type": "application/xml", + "ACCEPT": "application/xml"}) + + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(401, int(resp['status'])) + + def test_tenant_create_disabled_token(self): + h = httplib2.Http(".cache") + resp, content = create_tenant("test_tenant", str(self.auth_token)) + if int(resp['status']) == 200: + self.tenant = content['tenant']['id'] + + url = '%stenants' % (URL) + body = '{"tenant": { "id": "%s", \ + "description": "A description ...", "enabled"\ + :true } }' % self.tenant + resp, content = h.request(url, "POST", body=body,\ + headers={"Content-Type": "application/json",\ + "X-Auth-Token": self.disabled_token}) + + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(403, int(resp['status'])) + + def test_tenant_create_disabled_token_xml(self): + h = httplib2.Http(".cache") + resp, content = create_tenant_xml("test_tenant", str(self.auth_token)) + content = etree.fromstring(content) + if int(resp['status']) == 200: + self.tenant = content.get('id') + + url = '%stenants' % (URL) + body = ' \ + \ + A description... \ + ' % self.tenant + resp, content = h.request(url, "POST", body=body,\ + headers={"Content-Type": "application/xml", + "X-Auth-Token": self.disabled_token, + "ACCEPT": "application/xml"}) + + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(403, int(resp['status'])) + + def test_tenant_create_invalid_token(self): + h = httplib2.Http(".cache") + resp, content = create_tenant("test_tenant", str(self.auth_token)) + if int(resp['status']) == 200: + self.tenant = content['tenant']['id'] + + url = '%stenants' % (URL) + body = '{"tenant": { "id": "%s", \ + "description": "A description ...", "enabled"\ + :true } }' % self.tenant + resp, content = h.request(url, "POST", body=body,\ + headers={"Content-Type": "application/json",\ + "X-Auth-Token": 'nonexsitingtoken'}) + + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(401, int(resp['status'])) + + def test_tenant_create_invalid_token_xml(self): + h = httplib2.Http(".cache") + resp, content = create_tenant_xml("test_tenant", str(self.auth_token)) + content = etree.fromstring(content) + if int(resp['status']) == 200: + self.tenant = content.get('id') + + url = '%stenants' % (URL) + body = ' \ + \ + A description... \ + ' % self.tenant + resp, content = h.request(url, "POST", body=body,\ + headers={"Content-Type": "application/xml",\ + "X-Auth-Token": 'nonexsitingtoken', + "ACCEPT": "application/xml"}) + + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(401, int(resp['status'])) + + +class get_tenants_test(tenant_test): + + def test_get_tenants(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants' % (URL) + #test for Content-Type = application/json + resp, content = h.request(url, "GET", body='{}',\ + headers={"Content-Type": "application/json",\ + "X-Auth-Token": self.auth_token}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(200, int(resp['status'])) + + def test_get_tenants_xml(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants' % (URL) + #test for Content-Type = application/json + resp, content = h.request(url, "GET", body='',\ + headers={"Content-Type": "application/xml",\ + "X-Auth-Token": self.auth_token, + "ACCEPT": "application/xml"}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(200, int(resp['status'])) + + def test_get_tenants_forbidden_token(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants' % (URL) + #test for Content-Type = application/json + resp, content = h.request(url, "GET", body='{}',\ + headers={"Content-Type": "application/json",\ + "X-Auth-Token": self.token}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(403, int(resp['status'])) + + def test_get_tenants_forbidden_token_xml(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants' % (URL) + #test for Content-Type = application/json + resp, content = h.request(url, "GET", body='',\ + headers={"Content-Type": "application/xml",\ + "X-Auth-Token": self.token, + "ACCEPT": "application/xml"}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(403, int(resp['status'])) + + def test_get_tenants_exp_token(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants' % (URL) + #test for Content-Type = application/json + resp, content = h.request(url, "GET", body='{}',\ + headers={"Content-Type": "application/json",\ + "X-Auth-Token": self.exp_auth_token}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(401, int(resp['status'])) + + def test_get_tenants_exp_token_xml(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants' % (URL) + #test for Content-Type = application/json + resp, content = h.request(url, "GET", body='',\ + headers={"Content-Type": "application/xml",\ + "X-Auth-Token": self.exp_auth_token, + "ACCEPT": "application/xml"}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(401, int(resp['status'])) + + +class get_tenant_test(tenant_test): + + def test_get_tenant(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants/%s' % (URL, self.tenant) + #test for Content-Type = application/json + resp, content = h.request(url, "GET", body='{}',\ + headers={"Content-Type": "application/json",\ + "X-Auth-Token": self.auth_token}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(200, int(resp['status'])) + + def test_get_tenant_xml(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants/%s' % (URL, self.tenant) + #test for Content-Type = application/json + resp, content = h.request(url, "GET", body='',\ + headers={"Content-Type": "application/xml",\ + "X-Auth-Token": self.auth_token, + "ACCEPT": "application/xml"}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(200, int(resp['status'])) + + def test_get_tenant_bad(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants/%s' % (URL, 'tenant_bad') + #test for Content-Type = application/json + resp, content = h.request(url, "GET", body='{',\ + headers={"Content-Type": "application/json",\ + "X-Auth-Token": self.auth_token}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(404, int(resp['status'])) + + def test_get_tenant_bad_xml(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants/%s' % (URL, 'tenant_bad') + #test for Content-Type = application/json + resp, content = h.request(url, "GET", body='{',\ + headers={"Content-Type": "application/xml",\ + "X-Auth-Token": self.auth_token, + "ACCEPT": "application/xml"}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(404, int(resp['status'])) + + def test_get_tenant_not_found(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants/NonexistingID' % (URL) + #test for Content-Type = application/json + resp, content = h.request(url, "GET", body='{}',\ + headers={"Content-Type": "application/json",\ + "X-Auth-Token": self.auth_token}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(404, int(resp['status'])) + + def test_get_tenant_not_found_xml(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants/NonexistingID' % (URL) + #test for Content-Type = application/json + resp, content = h.request(url, "GET", body='',\ + headers={"Content-Type": "application/xml",\ + "X-Auth-Token": self.auth_token, + "ACCEPT": "application/xml"}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(404, int(resp['status'])) + + +class update_tenant_test(tenant_test): + + def test_update_tenant(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants/%s' % (URL, self.tenant) + data = '{"tenant": { "description": "A NEW description..." ,\ + "enabled":true }}' + #test for Content-Type = application/json + resp, content = h.request(url, "PUT", body=data,\ + headers={"Content-Type": "application/json",\ + "X-Auth-Token": self.auth_token}) + body = json.loads(content) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(200, int(resp['status'])) + self.assertEqual(int(self.tenant), int(body['tenant']['id'])) + self.assertEqual('A NEW description...', \ + body['tenant']['description']) + + def test_update_tenant_xml(self): + h = httplib2.Http(".cache") + resp, content = create_tenant_xml(self.tenant, str(self.auth_token)) + url = '%stenants/%s' % (URL, self.tenant) + data = ' \ + \ + A NEW description... \ + ' + + #test for Content-Type = application/json + resp, content = h.request(url, "PUT", body=data,\ + headers={"Content-Type": "application/xml",\ + "X-Auth-Token": self.auth_token, + "ACCEPT": "application/xml"}) + body = etree.fromstring(content) + desc = body.find("{http://docs.openstack.org/idm/api/v1.0}description") + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(200, int(resp['status'])) + self.assertEqual(int(self.tenant), int(body.get('id'))) + self.assertEqual('A NEW description...', \ + desc.text) + + def test_update_tenant_bad(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants/%s' % (URL, self.tenant) + data = '{"tenant": { "description_bad": "A NEW description...",\ + "enabled":true }}' + #test for Content-Type = application/json + + resp, content = h.request(url, "PUT", body=data,\ + headers={"Content-Type": "application/json",\ + "X-Auth-Token": self.auth_token}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(400, int(resp['status'])) + + def test_update_tenant_bad_xml(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants/%s' % (URL, self.tenant) + data = ' \ + \ + A NEW description... \ + ' + #test for Content-Type = application/json + resp, content = h.request(url, "PUT", body=data,\ + headers={"Content-Type": "application/xml",\ + "X-Auth-Token": self.auth_token, + "ACCEPT": "application/xml"}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(400, int(resp['status'])) + + def test_update_tenant_not_found(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants/NonexistingID' % (URL) + data = '{"tenant": { "description": "A NEW description...",\ + "enabled":true }}' + #test for Content-Type = application/json + resp, content = h.request(url, "GET", body=data,\ + headers={"Content-Type": "application/json",\ + "X-Auth-Token": self.auth_token}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(404, int(resp['status'])) + + def test_update_tenant_not_found_xml(self): + h = httplib2.Http(".cache") + resp, content = create_tenant(self.tenant, str(self.auth_token)) + url = '%stenants/NonexistingID' % (URL) + data = ' \ + \ + A NEW description... \ + ' + #test for Content-Type = application/json + resp, content = h.request(url, "GET", body=data,\ + headers={"Content-Type": "application/xml",\ + "X-Auth-Token": self.auth_token, + "ACCEPT": "application/xml"}) + if int(resp['status']) == 500: + self.fail('IDM fault') + elif int(resp['status']) == 503: + self.fail('Service Not Available') + self.assertEqual(404, int(resp['status'])) + + +class delete_tenant_test(tenant_test): + + def test_delete_tenant_not_found(self): + #resp,content=create_tenant("test_tenant_delete", str(self.auth_token)) + resp, content = delete_tenant("test_tenant_delete111", \ + str(self.auth_token)) + self.assertEqual(404, int(resp['status'])) + + def test_delete_tenant_not_found_xml(self): + #resp,content=create_tenant("test_tenant_delete", str(self.auth_token)) + resp, content = delete_tenant_xml("test_tenant_delete111", \ + str(self.auth_token)) + self.assertEqual(404, int(resp['status'])) + + def test_delete_tenant(self): + resp, content = create_tenant("test_tenant_delete", \ + str(self.auth_token)) + resp, content = delete_tenant("test_tenant_delete", \ + str(self.auth_token)) + self.assertEqual(204, int(resp['status'])) + + def test_delete_tenant_xml(self): + resp, content = create_tenant_xml("test_tenant_delete", \ + str(self.auth_token)) + resp, content = delete_tenant_xml("test_tenant_delete", \ + str(self.auth_token)) + self.assertEqual(204, int(resp['status'])) + +if __name__ == '__main__': + unittest.main() -- cgit