From af2cbe8a5bec2b7971de137416d6e62ac1b96498 Mon Sep 17 00:00:00 2001 From: Prashanth Pai Date: Mon, 23 Sep 2013 11:47:21 +0530 Subject: Minor swiftkerbauth changes * Replaced python-webob with swift.common.swob * Use swift memcached instead of python memcached * Added optional debugging headers to swift-auth script * Swiftkerbauth and Apachekerbauth are now a single RPM * Updates to httpd conf file to specify Kerberos principal * Added setupy.py, makerpm.sh, .gitignore and MANIFEST.in * RPM is now generated by bdist_rpm using setup.py and not from spec files TODO -> Documentation changes in doc/ * Steps to setup kerberos environment * Swiftkerbauth usage and examples -> Testing swiftkerbauth * Investigate borrowing tests from tempauth.py and its dependencies * Write a python client script to test swiftkerbauth Signed-off-by: Prashanth Pai --- .gitignore | 34 ++ COPYING | 202 ------------ LICENSE | 191 ++++++++++++ MANIFEST.in | 4 + README | 301 +----------------- apachekerbauth/apachekerbauth.spec | 50 --- .../etc/httpd/conf.d/swift-auth.conf | 9 - .../apachekerbauth/var/www/cgi-bin/memcached.py | 318 ------------------- .../apachekerbauth/var/www/cgi-bin/swift-auth | 117 ------- apachekerbauth/build.sh | 7 - apachekerbauth/etc/httpd/conf.d/swift-auth.conf | 12 + apachekerbauth/var/www/cgi-bin/swift-auth | 123 ++++++++ doc/DOCUMENTATION | 299 ++++++++++++++++++ makerpm.sh | 8 + setup.py | 38 +++ swiftkerbauth/__init__.py | 17 + swiftkerbauth/build.sh | 7 - swiftkerbauth/kerbauth.py | 328 ++++++++++++++++++++ swiftkerbauth/swiftkerbauth.spec | 54 ---- swiftkerbauth/swiftkerbauth/swiftkerbauth.py | 345 --------------------- 20 files changed, 1057 insertions(+), 1407 deletions(-) create mode 100644 .gitignore delete mode 100644 COPYING create mode 100644 LICENSE create mode 100644 MANIFEST.in delete mode 100644 apachekerbauth/apachekerbauth.spec delete mode 100644 apachekerbauth/apachekerbauth/etc/httpd/conf.d/swift-auth.conf delete mode 100644 apachekerbauth/apachekerbauth/var/www/cgi-bin/memcached.py delete mode 100755 apachekerbauth/apachekerbauth/var/www/cgi-bin/swift-auth delete mode 100755 apachekerbauth/build.sh create mode 100644 apachekerbauth/etc/httpd/conf.d/swift-auth.conf create mode 100755 apachekerbauth/var/www/cgi-bin/swift-auth create mode 100644 doc/DOCUMENTATION create mode 100755 makerpm.sh create mode 100644 setup.py create mode 100644 swiftkerbauth/__init__.py delete mode 100755 swiftkerbauth/build.sh create mode 100644 swiftkerbauth/kerbauth.py delete mode 100644 swiftkerbauth/swiftkerbauth.spec delete mode 100644 swiftkerbauth/swiftkerbauth/swiftkerbauth.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..835fe8b --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +sdist +develop-eggs +.installed.cfg +lib +lib64 + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject diff --git a/COPYING b/COPYING deleted file mode 100644 index d645695..0000000 --- a/COPYING +++ /dev/null @@ -1,202 +0,0 @@ - - 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/LICENSE b/LICENSE new file mode 100644 index 0000000..37ec93a --- /dev/null +++ b/LICENSE @@ -0,0 +1,191 @@ +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: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +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 +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/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..cadec55 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include LICENSE README +recursive-include swiftkerbauth *.py +graft doc +graft apachekerbauth diff --git a/README b/README index 34b4ea7..5200e57 100644 --- a/README +++ b/README @@ -1,299 +1,4 @@ -Kerberos Authentication Filter for Red Hat Storage and OpenStack Swift ----------------------------------------------------------------------- +swiftkerbauth +============= -Red Hat Storage not only provides file system access to its data, but -also object-level access. The latter is implemented with OpenStack -Swift, and allows containers and objects to be stored and retrieved -with an HTTP-based API. - -Red Hat Storage 2.0 comes with a simple authentication filter that -defines user accounts as a static list in the Swift configuration -file. For this project, we implemented a new authentication filter -that uses Kerberos tickets for single sign on authentication, and -grants administrator permissions based on the user's group membership -in a directory service like Red Hat Enterprise Linux Identity -Management or Microsoft Active Directory. - -* Building - -To build the swiftkerbauth and apachekerbauth RPM packages, change -into the respective directory and run - - ./build.sh - -* Installation - -** Swift Server - -Install the swiftkerbauth RPM on all Red Hat Storage nodes that will -provide object-level access via Swift. - -To active the Kerberos authentication filter, add "kerbauth" in the -/etc/swift/proxy-server.conf pipeline parameter: - - [pipeline:main] - pipeline = healthcheck cache kerbauth proxy-server - -Set the URL of the Apache server that will be used for authentication -with the ext_authentication_url parameter in the same file: - - [filter:kerbauth] - paste.filter_factory = swiftkerbauth:filter_factory - ext_authentication_url = http://AUTHENTICATION_SERVER/cgi-bin/swift-auth - -If the Swift server is not one of your Gluster nodes, edit -/etc/swift/fs.conf and change the following lines in the DEFAULT -section: - - mount_ip = RHS_NODE_HOSTNAME - remote_cluster = yes - -Activate the changes by running - - swift-init main restart - -For troubleshooting, check /var/log/messages. - -** Authentication Server - -On the authentication server, install the apachekerbauth package. - -Edit /etc/httpd/conf.d/swift-auth.conf and set the KrbAuthRealms and -Krb5KeyTab parameters. - -The keytab must contain a HTTP/$HOSTNAME principal. Usually, you will -have to create the Kerberos principal on the KDC, export it, and copy -it to a keytab file on the Apache server. - -If SELinux is enabled, allow Apache to connect to memcache and -activate the changes by running - - setsebool -P httpd_can_network_connect 1 - setsebool -P httpd_can_network_memcache 1 - - service httpd reload - -For troubleshooting, see /var/log/httpd/error_log. - -* Testing - -The tests were done with curl on a machine set up as an IDM client, -using the Gluster volume rhs_ufo1. - -In IDM, we created the following user groups: - -- auth_reseller_admin - Users in this group get full access to all Swift accounts. - -- auth_rhs_ufo1 - Users in this group get full access to the rhs_ufo1 Swift account. - -Next, we created the following users in IDM: - -- auth_admin - Member of the auth_reseller_admin group - -- rhs_ufo1_admin - Member of the auth_rhs_ufo1 group - -- jsmith - No relevant group membership - -The authentication tokens were then retrieved with the following -commands: - - kinit auth_admin - curl -v -u : --negotiate --location-trusted \ - http://rhs1.example.com:8080/auth/v1.0 - - kinit rhs_ufo1_admin - curl -v -u : --negotiate --location-trusted \ - http://rhs1.example.com:8080/auth/v1.0 - - kinit jsmith - curl -v -u : --negotiate --location-trusted \ - http://rhs1.example.com:8080/auth/v1.0 - -Each of these commands should output the following two lines: - -< X-Auth-Token: AUTH_tk4097146ed3814e026209556eeb121fe0 -... -
1365195860 / auth_admin,auth_reseller_admin
- -The first line contains the authentication token that is used in -subsequent requests. - -The second line is printed by the swift-auth CGI script for debugging -- it lists the token expiration (in seconds since January 1, 1970) and -the user's groups. - -Next, we try to get information about the Swift account, replacing the -AUTH_tk* with one of the tokens we got with the commands above. This -should display statistics, and the list of container names when used -with the the admin users. For jsmith, you should get a 403 Forbidden -error. - - curl -v -X GET \ - -H 'X-Auth-Token: AUTH_tk4097146ed3814e026209556eeb121fe0' \ - http://rhs1.example.com:8080/v1/AUTH_rhs_ufo1 - -With one of the admin accounts, create a new container and a new -object in that container: - - curl -v -X PUT \ - -H 'X-Auth-Token: AUTH_tk4097146ed3814e026209556eeb121fe0' \ - http://rhs1.example.com:8080/v1/AUTH_rhs_ufo1/pictures - - curl -v -X PUT \ - -H 'X-Auth-Token: AUTH_tk4097146ed3814e026209556eeb121fe0' \ - -H 'Content-Length: 0' \ - http://rhs1.example.com:8080/v1/AUTH_rhs_ufo1/pictures/pic1.png - -Grant permission for jsmith to list and download objects from the -pictures container: - - curl -v -X POST \ - -H 'X-Auth-Token: AUTH_tkdbf7725c1e4ad1ebe9ab0d7098d425f2' \ - -H 'X-Container-Read: jsmith' \ - http://rhs1.example.com:8080/v1/AUTH_rhs_ufo1/pictures - -List the container contents using the authentication token for jsmith: - - curl -v -X GET \ - -H 'X-Auth-Token: AUTH_tkef8b417ac0c2a73a80ab3b8db85254e2' \ - http://rhs1.example.com:8080/v1/AUTH_rhs_ufo1/pictures - -Try to access a resource without an authentication token. This will -return a 303 redirect: - - curl -v -X GET \ - http://rhs1.example.com:8080/v1/AUTH_rhs_ufo1/pictures/pic1.png - -For curl to follow the redirect, you need to specify additional -options. With these, and with a current Kerberos ticket, you should -get the Kerberos user's cached authentication token, or a new one if -the previous token has expired. - - curl -v -u : --negotiate --location-trusted -X GET \ - http://rhs1.example.com:8080/v1/AUTH_rhs_ufo1/pictures/pic1.png - -* Implementation Details - -** Architecture - -The Swift API is HTTP-based. As described in the Swift documentation -[1], clients first make a request to an authentication URL, providing -a username and password. The reply contains a token which is used in -all subsequent requests. - -Swift has a chain of filters through which all client requests go. The -filters to use are configured with the pipeline parameter in -/etc/swift/proxy-server.conf: - - [pipeline:main] - pipeline = healthcheck cache tempauth proxy-server - -For the single sign authentication, we added a new filter called -"kerbauth" and put it into the filter pipeline in place of tempauth. - -The filter checks the URL for each client request. If it matches the -authentication URL, the client is redirected to a URL on a different -server. The URL is handled by a CGI script, which is set up to -authenticate the client with Kerberos negotiation, retrieve the user's -system groups [2], store them in a memcache ring shared with the Swift -server, and return the authentication token to the client. - -When the client provides the token as part of a resource request, the -kerbauth filter checks it against its memcache, grants administrator -rights based on the group membership retrieved from memcache, and -either grants or denies the resource access. - -[1] http://docs.openstack.org/api/openstack-object-storage/1.0/content/authentication-object-dev-guide.html - -[2] The user data and system groups are usually provided by Red Hat - Enterprise Linux identity Management or Microsoft Active - Directory. The script relies on the system configuration to be set - accordingly (/etc/nsswitch.conf). - -** swiftkerbauth.py - -The script /usr/lib/python2.6/site-packages/swiftkerbauth.py began as -a copy of the tempauth.py script from -/usr/lib/python2.6/site-packages/swift/common/middleware. It contains -the following modifications, among others: - -In the __init__ method, we read the ext_authentication_url parameter -from /etc/swift/proxy-server.conf. This is the URL that clients are -redirected to when they access either the Swift authentication URL, or -when they request a resource without a valid authentication token. - -The configuration in proxy-server.conf looks like this: - - [filter:kerbauth] - paste.filter_factory = swiftkerbauth:filter_factory - ext_authentication_url = http://rhel6-4.localdomain/cgi-bin/swift-auth - -The authorize method was changed so that global administrator rights -are granted if the user is a member of the auth_reseller_admin -group. Administrator rights for a specific account like vol1 are -granted if the user is a member of the auth_vol1 group. [3] - -The denied_response method was changed to return a HTTP redirect to -the external authentication URL if no valid token was provided by the -client. - -Most of the handle_get_token method was moved to the external -authentication script. This method now returns a HTTP redirect. - -In the __call__ and get_groups method, we removed support for the -HTTP_AUTHORIZATION header, which is only needed when Amazon S3 is -used. - -Like tempauth.py, swiftkerbauth.py uses a Swift wrapper to access -memcache. This wrapper converts the key to an MD5 hash and uses the -hash value to determine on which of a pre-defined list of servers to -store the data. - -[3] "auth" is the default reseller prefix, and would be different if - the reseller_prefix parameter in proxy-server.conf was set. - -** swift-auth CGI Script - -swift-auth resides on an Apache server and assumes that Apache is -configured to authenticate the user before this script is -executed. The script retrieves the username from the REMOTE_USER -environment variable, and checks if there already is a token for this -user in the memcache ring. If not, it generates a new one, retrieves -the user's system groups with "id -Gn USERNAME", stores this -information in the memcache ring, and returns the token to the client. - -For the Swift filter to be able to find the information, it was -important to use the Swift memcached module. Because we don't want to -require a full Swift installation on the authentication server, -/usr/lib/python2.6/site-packages/swift/common/memcached.py from the -Swift server was copied to /var/www/cgi-bin on the Apache server. - -To allow the CGI script to connect to memcache, the SELinux booleans -httpd_can_network_connect and httpd_can_network_memcache had to be -set. - -The tempauth filter uses the uuid module to generate token -strings. This module creates and runs temporary files, which leads to -AVC denial messages in /var/log/audit/audit.log when used from an -Apache CGI script. While the module still works, the audit log would -grow quickly. Instead of writing an SELinux policy module to allow or -to silently ignore these accesses, the swift-auth script uses the -"random" module for generating token strings. - -Red Hat Enterprise Linux 6 comes with Python 2.6 which only provides -method to list the locally defined user groups. To include groups from -Red Hat Enterprise Linux Identity Management and in the future from -Active Directory, the "id" command is run in a subprocess. - -* Reference Material - -Red Hat Storage Administration Guide: -https://access.redhat.com/knowledge/docs/Red_Hat_Storage/ - -Swift Documentation: -http://docs.openstack.org/developer/swift/ +Kerberos Authentication filter for Openstack Swift diff --git a/apachekerbauth/apachekerbauth.spec b/apachekerbauth/apachekerbauth.spec deleted file mode 100644 index cc6210a..0000000 --- a/apachekerbauth/apachekerbauth.spec +++ /dev/null @@ -1,50 +0,0 @@ -Name: apachekerbauth -Version: 1.0 -Release: 3 -Summary: Kerberos authentication filter for Swift - -Group: System Environment/Base -License: GPL -Source: %{name}.tar.gz -BuildRoot: %{_tmppath}/%{name}-root - -Requires: httpd >= 2.2.15 -Requires: mod_auth_kerb >= 5.4 - -%description -Python CGI script which is used by the swiftkerbauth package to -authenticate client requests using Kerberos. - -%prep -%setup -q -n %{name} - -%build - -%install -rm -rf $RPM_BUILD_ROOT - -mkdir -p \ - $RPM_BUILD_ROOT/etc/httpd/conf.d \ - $RPM_BUILD_ROOT/var/www/cgi-bin - -install -m 644 etc/httpd/conf.d/* \ - $RPM_BUILD_ROOT/etc/httpd/conf.d - -install -m 644 var/www/cgi-bin/memcached.py \ - $RPM_BUILD_ROOT/var/www/cgi-bin - -install var/www/cgi-bin/swift-auth \ - $RPM_BUILD_ROOT/var/www/cgi-bin - -%clean -rm -rf $RPM_BUILD_ROOT - -%files -%defattr(-,root,root,-) -%config /etc/httpd/conf.d/swift-auth.conf -/var/www/cgi-bin/memcached.py -/var/www/cgi-bin/swift-auth - -%changelog -* Fri Apr 5 2013 Carsten Clasohm - 1.0-1 -- initial build diff --git a/apachekerbauth/apachekerbauth/etc/httpd/conf.d/swift-auth.conf b/apachekerbauth/apachekerbauth/etc/httpd/conf.d/swift-auth.conf deleted file mode 100644 index ba2b249..0000000 --- a/apachekerbauth/apachekerbauth/etc/httpd/conf.d/swift-auth.conf +++ /dev/null @@ -1,9 +0,0 @@ - - AuthType Kerberos - AuthName "Swift Authentication" - KrbMethodNegotiate On - KrbMethodK5Passwd On - KrbAuthRealms EXAMPLE.COM - Krb5KeyTab /etc/httpd/conf/apache.keytab - require valid-user - diff --git a/apachekerbauth/apachekerbauth/var/www/cgi-bin/memcached.py b/apachekerbauth/apachekerbauth/var/www/cgi-bin/memcached.py deleted file mode 100644 index ecd9332..0000000 --- a/apachekerbauth/apachekerbauth/var/www/cgi-bin/memcached.py +++ /dev/null @@ -1,318 +0,0 @@ -# Copyright (c) 2010-2012 OpenStack, LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Lucid comes with memcached: v1.4.2. Protocol documentation for that -version is at: - -http://github.com/memcached/memcached/blob/1.4.2/doc/protocol.txt -""" - -import cPickle as pickle -import logging -import socket -import time -from bisect import bisect -from hashlib import md5 - -DEFAULT_MEMCACHED_PORT = 11211 - -CONN_TIMEOUT = 0.3 -IO_TIMEOUT = 2.0 -PICKLE_FLAG = 1 -NODE_WEIGHT = 50 -PICKLE_PROTOCOL = 2 -TRY_COUNT = 3 - -# if ERROR_LIMIT_COUNT errors occur in ERROR_LIMIT_TIME seconds, the server -# will be considered failed for ERROR_LIMIT_DURATION seconds. -ERROR_LIMIT_COUNT = 10 -ERROR_LIMIT_TIME = 60 -ERROR_LIMIT_DURATION = 60 - - -def md5hash(key): - return md5(key).hexdigest() - - -class MemcacheConnectionError(Exception): - pass - - -class MemcacheRing(object): - """ - Simple, consistent-hashed memcache client. - """ - - def __init__(self, servers, connect_timeout=CONN_TIMEOUT, - io_timeout=IO_TIMEOUT, tries=TRY_COUNT): - self._ring = {} - self._errors = dict(((serv, []) for serv in servers)) - self._error_limited = dict(((serv, 0) for serv in servers)) - for server in sorted(servers): - for i in xrange(NODE_WEIGHT): - self._ring[md5hash('%s-%s' % (server, i))] = server - self._tries = tries if tries <= len(servers) else len(servers) - self._sorted = sorted(self._ring.keys()) - self._client_cache = dict(((server, []) for server in servers)) - self._connect_timeout = connect_timeout - self._io_timeout = io_timeout - - def _exception_occurred(self, server, e, action='talking'): - if isinstance(e, socket.timeout): - logging.error(_("Timeout %(action)s to memcached: %(server)s"), - {'action': action, 'server': server}) - else: - logging.exception(_("Error %(action)s to memcached: %(server)s"), - {'action': action, 'server': server}) - now = time.time() - self._errors[server].append(time.time()) - if len(self._errors[server]) > ERROR_LIMIT_COUNT: - self._errors[server] = [err for err in self._errors[server] - if err > now - ERROR_LIMIT_TIME] - if len(self._errors[server]) > ERROR_LIMIT_COUNT: - self._error_limited[server] = now + ERROR_LIMIT_DURATION - logging.error(_('Error limiting server %s'), server) - - def _get_conns(self, key): - """ - Retrieves a server conn from the pool, or connects a new one. - Chooses the server based on a consistent hash of "key". - """ - pos = bisect(self._sorted, key) - served = [] - while len(served) < self._tries: - pos = (pos + 1) % len(self._sorted) - server = self._ring[self._sorted[pos]] - if server in served: - continue - served.append(server) - if self._error_limited[server] > time.time(): - continue - try: - fp, sock = self._client_cache[server].pop() - yield server, fp, sock - except IndexError: - try: - if ':' in server: - host, port = server.split(':') - else: - host = server - port = DEFAULT_MEMCACHED_PORT - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - sock.settimeout(self._connect_timeout) - sock.connect((host, int(port))) - sock.settimeout(self._io_timeout) - yield server, sock.makefile(), sock - except Exception, e: - self._exception_occurred(server, e, 'connecting') - - def _return_conn(self, server, fp, sock): - """ Returns a server connection to the pool """ - self._client_cache[server].append((fp, sock)) - - def set(self, key, value, serialize=True, timeout=0): - """ - Set a key/value pair in memcache - - :param key: key - :param value: value - :param serialize: if True, value is pickled before sending to memcache - :param timeout: ttl in memcache - """ - key = md5hash(key) - if timeout > 0: - timeout += time.time() - flags = 0 - if serialize: - value = pickle.dumps(value, PICKLE_PROTOCOL) - flags |= PICKLE_FLAG - for (server, fp, sock) in self._get_conns(key): - try: - sock.sendall('set %s %d %d %s noreply\r\n%s\r\n' % \ - (key, flags, timeout, len(value), value)) - self._return_conn(server, fp, sock) - return - except Exception, e: - self._exception_occurred(server, e) - - def get(self, key): - """ - Gets the object specified by key. It will also unpickle the object - before returning if it is pickled in memcache. - - :param key: key - :returns: value of the key in memcache - """ - key = md5hash(key) - value = None - for (server, fp, sock) in self._get_conns(key): - try: - sock.sendall('get %s\r\n' % key) - line = fp.readline().strip().split() - while line[0].upper() != 'END': - if line[0].upper() == 'VALUE' and line[1] == key: - size = int(line[3]) - value = fp.read(size) - if int(line[2]) & PICKLE_FLAG: - value = pickle.loads(value) - fp.readline() - line = fp.readline().strip().split() - self._return_conn(server, fp, sock) - return value - except Exception, e: - self._exception_occurred(server, e) - - def incr(self, key, delta=1, timeout=0): - """ - Increments a key which has a numeric value by delta. - If the key can't be found, it's added as delta or 0 if delta < 0. - If passed a negative number, will use memcached's decr. Returns - the int stored in memcached - Note: The data memcached stores as the result of incr/decr is - an unsigned int. decr's that result in a number below 0 are - stored as 0. - - :param key: key - :param delta: amount to add to the value of key (or set as the value - if the key is not found) will be cast to an int - :param timeout: ttl in memcache - :raises MemcacheConnectionError: - """ - key = md5hash(key) - command = 'incr' - if delta < 0: - command = 'decr' - delta = str(abs(int(delta))) - for (server, fp, sock) in self._get_conns(key): - try: - sock.sendall('%s %s %s\r\n' % (command, key, delta)) - line = fp.readline().strip().split() - if line[0].upper() == 'NOT_FOUND': - add_val = delta - if command == 'decr': - add_val = '0' - sock.sendall('add %s %d %d %s\r\n%s\r\n' % \ - (key, 0, timeout, len(add_val), add_val)) - line = fp.readline().strip().split() - if line[0].upper() == 'NOT_STORED': - sock.sendall('%s %s %s\r\n' % (command, key, delta)) - line = fp.readline().strip().split() - ret = int(line[0].strip()) - else: - ret = int(add_val) - else: - ret = int(line[0].strip()) - self._return_conn(server, fp, sock) - return ret - except Exception, e: - self._exception_occurred(server, e) - raise MemcacheConnectionError("No Memcached connections succeeded.") - - def decr(self, key, delta=1, timeout=0): - """ - Decrements a key which has a numeric value by delta. Calls incr with - -delta. - - :param key: key - :param delta: amount to subtract to the value of key (or set the - value to 0 if the key is not found) will be cast to - an int - :param timeout: ttl in memcache - :raises MemcacheConnectionError: - """ - self.incr(key, delta=-delta, timeout=timeout) - - def delete(self, key): - """ - Deletes a key/value pair from memcache. - - :param key: key to be deleted - """ - key = md5hash(key) - for (server, fp, sock) in self._get_conns(key): - try: - sock.sendall('delete %s noreply\r\n' % key) - self._return_conn(server, fp, sock) - return - except Exception, e: - self._exception_occurred(server, e) - - def set_multi(self, mapping, server_key, serialize=True, timeout=0): - """ - Sets multiple key/value pairs in memcache. - - :param mapping: dictonary of keys and values to be set in memcache - :param servery_key: key to use in determining which server in the ring - is used - :param serialize: if True, value is pickled before sending to memcache - :param timeout: ttl for memcache - """ - server_key = md5hash(server_key) - if timeout > 0: - timeout += time.time() - msg = '' - for key, value in mapping.iteritems(): - key = md5hash(key) - flags = 0 - if serialize: - value = pickle.dumps(value, PICKLE_PROTOCOL) - flags |= PICKLE_FLAG - msg += ('set %s %d %d %s noreply\r\n%s\r\n' % - (key, flags, timeout, len(value), value)) - for (server, fp, sock) in self._get_conns(server_key): - try: - sock.sendall(msg) - self._return_conn(server, fp, sock) - return - except Exception, e: - self._exception_occurred(server, e) - - def get_multi(self, keys, server_key): - """ - Gets multiple values from memcache for the given keys. - - :param keys: keys for values to be retrieved from memcache - :param servery_key: key to use in determining which server in the ring - is used - :returns: list of values - """ - server_key = md5hash(server_key) - keys = [md5hash(key) for key in keys] - for (server, fp, sock) in self._get_conns(server_key): - try: - sock.sendall('get %s\r\n' % ' '.join(keys)) - line = fp.readline().strip().split() - responses = {} - while line[0].upper() != 'END': - if line[0].upper() == 'VALUE': - size = int(line[3]) - value = fp.read(size) - if int(line[2]) & PICKLE_FLAG: - value = pickle.loads(value) - responses[line[1]] = value - fp.readline() - line = fp.readline().strip().split() - values = [] - for key in keys: - if key in responses: - values.append(responses[key]) - else: - values.append(None) - self._return_conn(server, fp, sock) - return values - except Exception, e: - self._exception_occurred(server, e) diff --git a/apachekerbauth/apachekerbauth/var/www/cgi-bin/swift-auth b/apachekerbauth/apachekerbauth/var/www/cgi-bin/swift-auth deleted file mode 100755 index 30f98b5..0000000 --- a/apachekerbauth/apachekerbauth/var/www/cgi-bin/swift-auth +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/python - -# Copyright (c) 2013 Red Hat, Inc. -# -# 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. - -# Requires the python-memcached package to be installed. -# -# Requires the following command to be run: -# setsebool -P httpd_can_network_connect 1 -# setsebool -P httpd_can_network_memcache 1 - -import cgi -import json -from memcached import MemcacheRing -import os -import random -import re -import subprocess -from time import time - -# After how many seconds the cached information about an authentication -# token is discarded. -TOKEN_LIFE = 86400 - -# This is used as a prefix for tokens and memcache keys. We use the default -# value from the Swift tempauth filter. In the future, this should be turned -# into a configuration parameter. -RESELLER_PREFIX = 'AUTH_' - -MEMCACHE_SERVERS = ['rhs1.example.com:11211'] - -def main(): - remote_user = os.environ['REMOTE_USER'] - - matches = re.match('([^@]+)@.*', remote_user) - if not matches: - raise RuntimeError("Malformed REMOTE_USER \"%s\"" % remote_user) - - username = matches.group(1) - - mc = MemcacheRing(MEMCACHE_SERVERS) - - # Check if we already got a token for this user. - memcache_user_key = '%s/user/%s' % (RESELLER_PREFIX, username) - token = None - candidate_token = mc.get(memcache_user_key) - if candidate_token: - memcache_token_key = '%s/token/%s' % (RESELLER_PREFIX, candidate_token) - cached_auth_data = mc.get(memcache_token_key) - if cached_auth_data: - expires, groups = cached_auth_data - if expires > time(): - token = candidate_token - - if not token: - # We don't use uuid.uuid4() here because importing the uuid module - # causes (harmless) SELinux denials in the audit log on RHEL 6. If this - # is a security concern, a custom SELinux policy module could be written - # to not log those denials. - r = random.SystemRandom() - token = '%stk%s' % (RESELLER_PREFIX, - ''.join(r.choice('abcdef0123456789') for x in range(32))) - - # Retrieve the numerical group IDs. We cannot list the group names - # because group names from Active Directory may contain spaces, and - # we wouldn't be able to split the list of group names into its - # elements. - p = subprocess.Popen(['id', '-G', username], stdout=subprocess.PIPE) - if p.wait() != 0: - raise RuntimeError("Failure running id -G for %s" % remote_user) - - (p_stdout, p_stderr) = p.communicate() - - # Convert the group numbers into group names. - groups = [] - for gid in p_stdout.strip().split(" "): - groups.append(grp.getgrgid(int(gid))[0]) - - # The first element of the list is considered a unique identifier - # for the user. We add the username to accomplish this. - if username in groups: - groups.remove(username) - groups = [username] + groups - - groups = ','.join(groups) - - expires = time() + TOKEN_LIFE - auth_data = (expires, groups) - - memcache_token_key = "%s/token/%s" % (RESELLER_PREFIX, token) - mc.set(memcache_token_key, auth_data, timeout=TOKEN_LIFE) - - # Record the token with the user info for future use. - memcache_user_key = '%s/user/%s' % (RESELLER_PREFIX, username) - mc.set(memcache_user_key, token, timeout=TOKEN_LIFE) - - print "X-Auth-Token: %s\n" % token - - # For debugging. - print "
%i / %s
" % mc.get(memcache_token_key) - -try: - print("Content-Type: text/html") - main() -except: - cgi.print_exception() diff --git a/apachekerbauth/build.sh b/apachekerbauth/build.sh deleted file mode 100755 index 6db04ac..0000000 --- a/apachekerbauth/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -tar -cz --exclude=.svn -f ~/rpmbuild/SOURCES/apachekerbauth.tar.gz apachekerbauth - -rpmbuild --target noarch --clean -bb apachekerbauth.spec - -rm ~/rpmbuild/SOURCES/apachekerbauth.tar.gz diff --git a/apachekerbauth/etc/httpd/conf.d/swift-auth.conf b/apachekerbauth/etc/httpd/conf.d/swift-auth.conf new file mode 100644 index 0000000..68472d8 --- /dev/null +++ b/apachekerbauth/etc/httpd/conf.d/swift-auth.conf @@ -0,0 +1,12 @@ + + AuthType Kerberos + AuthName "Swift Authentication" + KrbMethodNegotiate On + KrbMethodK5Passwd On + KrbSaveCredentials On + KrbServiceName HTTP/client.example.com + KrbAuthRealms EXAMPLE.COM + Krb5KeyTab /etc/httpd/conf/http.keytab + KrbVerifyKDC Off + Require valid-user + diff --git a/apachekerbauth/var/www/cgi-bin/swift-auth b/apachekerbauth/var/www/cgi-bin/swift-auth new file mode 100755 index 0000000..6173408 --- /dev/null +++ b/apachekerbauth/var/www/cgi-bin/swift-auth @@ -0,0 +1,123 @@ +#!/usr/bin/python + +# Copyright (c) 2013 Red Hat, Inc. +# +# 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. + +# Requires the following command to be run: +# setsebool -P httpd_can_network_connect 1 +# setsebool -P httpd_can_network_memcache 1 + +import cgi +from swift.common.memcached import MemcacheRing +import os +import grp +import random +import re +import subprocess +from time import time, ctime + +# After how many seconds the cached information about an authentication +# token is discarded. +TOKEN_LIFE = 86400 + +# This is used as a prefix for tokens and memcache keys. We use the default +# value from the Swift tempauth filter. In the future, this should be turned +# into a configuration parameter. +RESELLER_PREFIX = 'AUTH_' + +MEMCACHE_SERVERS = ['127.0.0.1:11211'] + +DEBUG_HEADERS = True + +def main(): + remote_user = os.environ['REMOTE_USER'] + matches = re.match('([^@]+)@.*', remote_user) + if not matches: + raise RuntimeError("Malformed REMOTE_USER \"%s\"" % remote_user) + + username = matches.group(1) + + mc = MemcacheRing(MEMCACHE_SERVERS) + + # Check if we already got a token for this user. + memcache_user_key = '%s/user/%s' % (RESELLER_PREFIX, username) + token = None + candidate_token = mc.get(memcache_user_key) + if candidate_token: + memcache_token_key = '%s/token/%s' % (RESELLER_PREFIX, candidate_token) + cached_auth_data = mc.get(memcache_token_key) + if cached_auth_data: + expires, groups = cached_auth_data + if expires > time(): + token = candidate_token + + if not token: + # We don't use uuid.uuid4() here because importing the uuid module + # causes (harmless) SELinux denials in the audit log on RHEL 6. If this + # is a security concern, a custom SELinux policy module could be + # written to not log those denials. + r = random.SystemRandom() + token = '%stk%s' % \ + (RESELLER_PREFIX, + ''.join(r.choice('abcdef0123456789') for x in range(32))) + + # Retrieve the numerical group IDs. We cannot list the group names + # because group names from Active Directory may contain spaces, and + # we wouldn't be able to split the list of group names into its + # elements. + p = subprocess.Popen(['id', '-G', username], stdout=subprocess.PIPE) + if p.wait() != 0: + raise RuntimeError("Failure running id -G for %s" % remote_user) + + (p_stdout, p_stderr) = p.communicate() + + # Convert the group numbers into group names. + groups = [] + for gid in p_stdout.strip().split(" "): + groups.append(grp.getgrgid(int(gid))[0]) + + # The first element of the list is considered a unique identifier + # for the user. We add the username to accomplish this. + if username in groups: + groups.remove(username) + groups = [username] + groups + + groups = ','.join(groups) + + expires = time() + TOKEN_LIFE + auth_data = (expires, groups) + + memcache_token_key = "%s/token/%s" % (RESELLER_PREFIX, token) + mc.set(memcache_token_key, auth_data, timeout=TOKEN_LIFE) + + # Record the token with the user info for future use. + memcache_user_key = '%s/user/%s' % (RESELLER_PREFIX, username) + mc.set(memcache_user_key, token, timeout=TOKEN_LIFE) + + print "X-Auth-Token: %s" % token + + # For debugging. + if DEBUG_HEADERS: + print "X-Debug-Remote-User: %s" % username + print "X-Debug-Groups: %s" % groups + print "X-Debug-Token-Life: %ss" % TOKEN_LIFE + print "X-Debug-Token-Expires: %s" % ctime(expires) + + print "" + +try: + print("Content-Type: text/html") + main() +except: + cgi.print_exception() diff --git a/doc/DOCUMENTATION b/doc/DOCUMENTATION new file mode 100644 index 0000000..5b36b24 --- /dev/null +++ b/doc/DOCUMENTATION @@ -0,0 +1,299 @@ +Kerberos Authentication Filter for Red Hat Storage and OpenStack Swift +---------------------------------------------------------------------- + +Red Hat Storage not only provides file system access to its data, but +also object-level access. The latter is implemented with OpenStack +Swift, and allows containers and objects to be stored and retrieved +with an HTTP-based API. + +Red Hat Storage 2.0 comes with a simple authentication filter that +defines user accounts as a static list in the Swift configuration +file. For this project, we implemented a new authentication filter +that uses Kerberos tickets for single sign on authentication, and +grants administrator permissions based on the user's group membership +in a directory service like Red Hat Enterprise Linux Identity +Management or Microsoft Active Directory. + +* Building + +To build the swiftkerbauth and apachekerbauth RPM packages, change +into the respective directory and run + + ./build.sh + +* Installation + +** Swift Server + +Install the swiftkerbauth RPM on all Red Hat Storage nodes that will +provide object-level access via Swift. + +To active the Kerberos authentication filter, add "kerbauth" in the +/etc/swift/proxy-server.conf pipeline parameter: + + [pipeline:main] + pipeline = healthcheck cache kerbauth proxy-server + +Set the URL of the Apache server that will be used for authentication +with the ext_authentication_url parameter in the same file: + + [filter:kerbauth] + paste.filter_factory = swiftkerbauth:filter_factory + ext_authentication_url = http://AUTHENTICATION_SERVER/cgi-bin/swift-auth + +If the Swift server is not one of your Gluster nodes, edit +/etc/swift/fs.conf and change the following lines in the DEFAULT +section: + + mount_ip = RHS_NODE_HOSTNAME + remote_cluster = yes + +Activate the changes by running + + swift-init main restart + +For troubleshooting, check /var/log/messages. + +** Authentication Server + +On the authentication server, install the apachekerbauth package. + +Edit /etc/httpd/conf.d/swift-auth.conf and set the KrbAuthRealms and +Krb5KeyTab parameters. + +The keytab must contain a HTTP/$HOSTNAME principal. Usually, you will +have to create the Kerberos principal on the KDC, export it, and copy +it to a keytab file on the Apache server. + +If SELinux is enabled, allow Apache to connect to memcache and +activate the changes by running + + setsebool -P httpd_can_network_connect 1 + setsebool -P httpd_can_network_memcache 1 + + service httpd reload + +For troubleshooting, see /var/log/httpd/error_log. + +* Testing + +The tests were done with curl on a machine set up as an IDM client, +using the Gluster volume rhs_ufo1. + +In IDM, we created the following user groups: + +- auth_reseller_admin + Users in this group get full access to all Swift accounts. + +- auth_rhs_ufo1 + Users in this group get full access to the rhs_ufo1 Swift account. + +Next, we created the following users in IDM: + +- auth_admin + Member of the auth_reseller_admin group + +- rhs_ufo1_admin + Member of the auth_rhs_ufo1 group + +- jsmith + No relevant group membership + +The authentication tokens were then retrieved with the following +commands: + + kinit auth_admin + curl -v -u : --negotiate --location-trusted \ + http://rhs1.example.com:8080/auth/v1.0 + + kinit rhs_ufo1_admin + curl -v -u : --negotiate --location-trusted \ + http://rhs1.example.com:8080/auth/v1.0 + + kinit jsmith + curl -v -u : --negotiate --location-trusted \ + http://rhs1.example.com:8080/auth/v1.0 + +Each of these commands should output the following two lines: + +< X-Auth-Token: AUTH_tk4097146ed3814e026209556eeb121fe0 +... +
1365195860 / auth_admin,auth_reseller_admin
+ +The first line contains the authentication token that is used in +subsequent requests. + +The second line is printed by the swift-auth CGI script for debugging +- it lists the token expiration (in seconds since January 1, 1970) and +the user's groups. + +Next, we try to get information about the Swift account, replacing the +AUTH_tk* with one of the tokens we got with the commands above. This +should display statistics, and the list of container names when used +with the the admin users. For jsmith, you should get a 403 Forbidden +error. + + curl -v -X GET \ + -H 'X-Auth-Token: AUTH_tk4097146ed3814e026209556eeb121fe0' \ + http://rhs1.example.com:8080/v1/AUTH_rhs_ufo1 + +With one of the admin accounts, create a new container and a new +object in that container: + + curl -v -X PUT \ + -H 'X-Auth-Token: AUTH_tk4097146ed3814e026209556eeb121fe0' \ + http://rhs1.example.com:8080/v1/AUTH_rhs_ufo1/pictures + + curl -v -X PUT \ + -H 'X-Auth-Token: AUTH_tk4097146ed3814e026209556eeb121fe0' \ + -H 'Content-Length: 0' \ + http://rhs1.example.com:8080/v1/AUTH_rhs_ufo1/pictures/pic1.png + +Grant permission for jsmith to list and download objects from the +pictures container: + + curl -v -X POST \ + -H 'X-Auth-Token: AUTH_tkdbf7725c1e4ad1ebe9ab0d7098d425f2' \ + -H 'X-Container-Read: jsmith' \ + http://rhs1.example.com:8080/v1/AUTH_rhs_ufo1/pictures + +List the container contents using the authentication token for jsmith: + + curl -v -X GET \ + -H 'X-Auth-Token: AUTH_tkef8b417ac0c2a73a80ab3b8db85254e2' \ + http://rhs1.example.com:8080/v1/AUTH_rhs_ufo1/pictures + +Try to access a resource without an authentication token. This will +return a 303 redirect: + + curl -v -X GET \ + http://rhs1.example.com:8080/v1/AUTH_rhs_ufo1/pictures/pic1.png + +For curl to follow the redirect, you need to specify additional +options. With these, and with a current Kerberos ticket, you should +get the Kerberos user's cached authentication token, or a new one if +the previous token has expired. + + curl -v -u : --negotiate --location-trusted -X GET \ + http://rhs1.example.com:8080/v1/AUTH_rhs_ufo1/pictures/pic1.png + +* Implementation Details + +** Architecture + +The Swift API is HTTP-based. As described in the Swift documentation +[1], clients first make a request to an authentication URL, providing +a username and password. The reply contains a token which is used in +all subsequent requests. + +Swift has a chain of filters through which all client requests go. The +filters to use are configured with the pipeline parameter in +/etc/swift/proxy-server.conf: + + [pipeline:main] + pipeline = healthcheck cache tempauth proxy-server + +For the single sign authentication, we added a new filter called +"kerbauth" and put it into the filter pipeline in place of tempauth. + +The filter checks the URL for each client request. If it matches the +authentication URL, the client is redirected to a URL on a different +server. The URL is handled by a CGI script, which is set up to +authenticate the client with Kerberos negotiation, retrieve the user's +system groups [2], store them in a memcache ring shared with the Swift +server, and return the authentication token to the client. + +When the client provides the token as part of a resource request, the +kerbauth filter checks it against its memcache, grants administrator +rights based on the group membership retrieved from memcache, and +either grants or denies the resource access. + +[1] http://docs.openstack.org/api/openstack-object-storage/1.0/content/authentication-object-dev-guide.html + +[2] The user data and system groups are usually provided by Red Hat + Enterprise Linux identity Management or Microsoft Active + Directory. The script relies on the system configuration to be set + accordingly (/etc/nsswitch.conf). + +** swiftkerbauth.py + +The script /usr/lib/python2.6/site-packages/swiftkerbauth.py began as +a copy of the tempauth.py script from +/usr/lib/python2.6/site-packages/swift/common/middleware. It contains +the following modifications, among others: + +In the __init__ method, we read the ext_authentication_url parameter +from /etc/swift/proxy-server.conf. This is the URL that clients are +redirected to when they access either the Swift authentication URL, or +when they request a resource without a valid authentication token. + +The configuration in proxy-server.conf looks like this: + + [filter:kerbauth] + paste.filter_factory = swiftkerbauth:filter_factory + ext_authentication_url = http://rhel6-4.localdomain/cgi-bin/swift-auth + +The authorize method was changed so that global administrator rights +are granted if the user is a member of the auth_reseller_admin +group. Administrator rights for a specific account like vol1 are +granted if the user is a member of the auth_vol1 group. [3] + +The denied_response method was changed to return a HTTP redirect to +the external authentication URL if no valid token was provided by the +client. + +Most of the handle_get_token method was moved to the external +authentication script. This method now returns a HTTP redirect. + +In the __call__ and get_groups method, we removed support for the +HTTP_AUTHORIZATION header, which is only needed when Amazon S3 is +used. + +Like tempauth.py, swiftkerbauth.py uses a Swift wrapper to access +memcache. This wrapper converts the key to an MD5 hash and uses the +hash value to determine on which of a pre-defined list of servers to +store the data. + +[3] "auth" is the default reseller prefix, and would be different if + the reseller_prefix parameter in proxy-server.conf was set. + +** swift-auth CGI Script + +swift-auth resides on an Apache server and assumes that Apache is +configured to authenticate the user before this script is +executed. The script retrieves the username from the REMOTE_USER +environment variable, and checks if there already is a token for this +user in the memcache ring. If not, it generates a new one, retrieves +the user's system groups with "id -Gn USERNAME", stores this +information in the memcache ring, and returns the token to the client. + +For the Swift filter to be able to find the information, it was +important to use the Swift memcached module. Because we don't want to +require a full Swift installation on the authentication server, +/usr/lib/python2.6/site-packages/swift/common/memcached.py from the +Swift server was copied to /var/www/cgi-bin on the Apache server. + +To allow the CGI script to connect to memcache, the SELinux booleans +httpd_can_network_connect and httpd_can_network_memcache had to be +set. + +The tempauth filter uses the uuid module to generate token +strings. This module creates and runs temporary files, which leads to +AVC denial messages in /var/log/audit/audit.log when used from an +Apache CGI script. While the module still works, the audit log would +grow quickly. Instead of writing an SELinux policy module to allow or +to silently ignore these accesses, the swift-auth script uses the +"random" module for generating token strings. + +Red Hat Enterprise Linux 6 comes with Python 2.6 which only provides +method to list the locally defined user groups. To include groups from +Red Hat Enterprise Linux Identity Management and in the future from +Active Directory, the "id" command is run in a subprocess. + +* Reference Material + +Red Hat Storage Administration Guide: +https://access.redhat.com/knowledge/docs/Red_Hat_Storage/ + +Swift Documentation: +http://docs.openstack.org/developer/swift/ diff --git a/makerpm.sh b/makerpm.sh new file mode 100755 index 0000000..9635e14 --- /dev/null +++ b/makerpm.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# Creates swiftkerbauth RPMs in dist/ + +rm -rf dist/ swiftkerbauth.egg-info/ build/ +python setup.py bdist_rpm --requires="httpd >= 2.2.15, mod_auth_kerb >= 5.4" +rm -rf swiftkerbauth.egg-info/ build/ +echo "RPMS are now available in $PWD/dist/" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ce03189 --- /dev/null +++ b/setup.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python + +from setuptools import setup +from swiftkerbauth import __version__ + +setup( + name='swiftkerbauth', + version=__version__, + description='Kerberos authentication filter for Openstack Swift', + license='Apache License (2.0)', + author='Red Hat, Inc.', + author_email='gluster-users@gluster.org', + url='https://forge.gluster.org/swiftkerbauth', + packages=['swiftkerbauth'], + keywords='openstack swift kerberos', + install_requires=['swift>=1.9.1'], + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Environment :: OpenStack', + 'Intended Audience :: Information Technology', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + ], + data_files=[ + ('/var/www/cgi-bin', ['apachekerbauth/var/www/cgi-bin/swift-auth']), + ('/etc/httpd/conf.d', ['apachekerbauth/etc/httpd/conf.d/swift-auth.conf']), + ], + entry_points={ + 'paste.filter_factory': [ + 'kerbauth=swiftkerbauth.kerbauth:filter_factory', + ], + }, + ) diff --git a/swiftkerbauth/__init__.py b/swiftkerbauth/__init__.py new file mode 100644 index 0000000..eaa1d88 --- /dev/null +++ b/swiftkerbauth/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +# Copyright (c) 2013 Red Hat, Inc. +# +# 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. + +__version__ = "1.0.0" diff --git a/swiftkerbauth/build.sh b/swiftkerbauth/build.sh deleted file mode 100755 index 0daff96..0000000 --- a/swiftkerbauth/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -tar -cz --exclude=.svn -f ~/rpmbuild/SOURCES/swiftkerbauth.tar.gz swiftkerbauth - -rpmbuild --target noarch --clean -bb swiftkerbauth.spec - -rm ~/rpmbuild/SOURCES/swiftkerbauth.tar.gz diff --git a/swiftkerbauth/kerbauth.py b/swiftkerbauth/kerbauth.py new file mode 100644 index 0000000..a37f1f6 --- /dev/null +++ b/swiftkerbauth/kerbauth.py @@ -0,0 +1,328 @@ +# Copyright (c) 2013 Red Hat, Inc. +# +# 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 time import time +from traceback import format_exc +from eventlet import Timeout + +from swift.common.swob import Request +from swift.common.swob import HTTPBadRequest, HTTPForbidden, HTTPNotFound, \ + HTTPSeeOther + +from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed +from swift.common.utils import cache_from_env, get_logger, \ + split_path, config_true_value + + +class KerbAuth(object): + """ + Test authentication and authorization system. + + Add to your pipeline in proxy-server.conf, such as:: + + [pipeline:main] + pipeline = catch_errors cache kerbauth proxy-server + + Set account auto creation to true in proxy-server.conf:: + + [app:proxy-server] + account_autocreate = true + + And add a kerbauth filter section, such as:: + + [filter:kerbauth] + use = egg:swift#kerbauth + + See the proxy-server.conf-sample for more information. + + :param app: The next WSGI app in the pipeline + :param conf: The dict of configuration values + """ + + def __init__(self, app, conf): + self.app = app + self.conf = conf + self.logger = get_logger(conf, log_route='kerbauth') + self.log_headers = config_true_value(conf.get('log_headers', 'f')) + self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip() + if self.reseller_prefix and self.reseller_prefix[-1] != '_': + self.reseller_prefix += '_' + self.logger.set_statsd_prefix('kerbauth.%s' % ( + self.reseller_prefix if self.reseller_prefix else 'NONE',)) + self.auth_prefix = conf.get('auth_prefix', '/auth/') + if not self.auth_prefix or not self.auth_prefix.strip('/'): + self.logger.warning('Rewriting invalid auth prefix "%s" to ' + '"/auth/" (Non-empty auth prefix path ' + 'is required)' % self.auth_prefix) + self.auth_prefix = '/auth/' + if self.auth_prefix[0] != '/': + self.auth_prefix = '/' + self.auth_prefix + if self.auth_prefix[-1] != '/': + self.auth_prefix += '/' + self.token_life = int(conf.get('token_life', 86400)) + self.allow_overrides = config_true_value( + conf.get('allow_overrides', 't')) + self.storage_url_scheme = conf.get('storage_url_scheme', 'default') + self.ext_authentication_url = conf.get('ext_authentication_url') + if not self.ext_authentication_url: + raise RuntimeError("Missing filter parameter ext_authentication_" + "url in /etc/swift/proxy-server.conf") + + def __call__(self, env, start_response): + """ + Accepts a standard WSGI application call, authenticating the request + and installing callback hooks for authorization and ACL header + validation. For an authenticated request, REMOTE_USER will be set to a + comma separated list of the user's groups. + + If the request matches the self.auth_prefix, the request will be + routed through the internal auth request handler (self.handle). + This is to handle granting tokens, etc. + """ + if self.allow_overrides and env.get('swift.authorize_override', False): + return self.app(env, start_response) + if env.get('PATH_INFO', '').startswith(self.auth_prefix): + return self.handle(env, start_response) + token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) + if token and token.startswith(self.reseller_prefix): + groups = self.get_groups(env, token) + if groups: + user = groups and groups.split(',', 1)[0] or '' + trans_id = env.get('swift.trans_id') + self.logger.debug('User: %s uses token %s (trans_id %s)' % + (user, token, trans_id)) + env['REMOTE_USER'] = groups + env['swift.authorize'] = self.authorize + env['swift.clean_acl'] = clean_acl + if '.reseller_admin' in groups: + env['reseller_request'] = True + else: + # Invalid token (may be expired) + return HTTPSeeOther( + location=self.ext_authentication_url)(env, start_response) + else: + # With a non-empty reseller_prefix, I would like to be called + # back for anonymous access to accounts I know I'm the + # definitive auth for. + try: + version, rest = split_path(env.get('PATH_INFO', ''), + 1, 2, True) + except ValueError: + version, rest = None, None + self.logger.increment('errors') + # Not my token, not my account, I can't authorize this request, + # deny all is a good idea if not already set... + if 'swift.authorize' not in env: + env['swift.authorize'] = self.denied_response + + return self.app(env, start_response) + + def get_groups(self, env, token): + """ + Get groups for the given token. + + :param env: The current WSGI environment dictionary. + :param token: Token to validate and return a group string for. + + :returns: None if the token is invalid or a string containing a comma + separated list of groups the authenticated user is a member + of. The first group in the list is also considered a unique + identifier for that user. + """ + groups = None + memcache_client = cache_from_env(env) + if not memcache_client: + raise Exception('Memcache required') + memcache_token_key = '%s/token/%s' % (self.reseller_prefix, token) + cached_auth_data = memcache_client.get(memcache_token_key) + if cached_auth_data: + expires, groups = cached_auth_data + if expires < time(): + groups = None + + return groups + + def authorize(self, req): + """ + Returns None if the request is authorized to continue or a standard + WSGI response callable if not. + + Assumes that user groups are all lower case, which is true when Red Hat + Enterprise Linux Identity Management is used. + """ + try: + version, account, container, obj = req.split_path(1, 4, True) + except ValueError: + self.logger.increment('errors') + return HTTPNotFound(request=req) + + if not account or not account.startswith(self.reseller_prefix): + self.logger.debug("Account name: %s doesn't start with " + "reseller_prefix: %s." + % (account, self.reseller_prefix)) + return self.denied_response(req) + + user_groups = (req.remote_user or '').split(',') + account_user = user_groups[1] if len(user_groups) > 1 else None + # If the user is in the reseller_admin group for our prefix, he gets + # full access to all accounts we manage. For the default reseller + # prefix, the group name is auth_reseller_admin. + admin_group = ("%sreseller_admin" % self.reseller_prefix).lower() + if admin_group in user_groups and \ + account != self.reseller_prefix and \ + account[len(self.reseller_prefix)] != '.': + req.environ['swift_owner'] = True + return None + + # The "account" is part of the request URL, and already contains the + # reseller prefix, like in "/v1/AUTH_vol1/pictures/pic1.png". + if account.lower() in user_groups and \ + (req.method not in ('DELETE', 'PUT') or container): + # If the user is admin for the account and is not trying to do an + # account DELETE or PUT... + req.environ['swift_owner'] = True + self.logger.debug("User %s has admin authorizing." + % account_user) + return None + + if (req.environ.get('swift_sync_key') + and (req.environ['swift_sync_key'] == + req.headers.get('x-container-sync-key', None)) + and 'x-timestamp' in req.headers): + self.logger.debug("Allow request with container sync-key: %s." + % req.environ['swift_sync_key']) + return None + + if req.method == 'OPTIONS': + #allow OPTIONS requests to proceed as normal + self.logger.debug("Allow OPTIONS request.") + return None + + referrers, groups = parse_acl(getattr(req, 'acl', None)) + + if referrer_allowed(req.referer, referrers): + if obj or '.rlistings' in groups: + self.logger.debug("Allow authorizing %s via referer ACL." + % req.referer) + return None + + for user_group in user_groups: + if user_group in groups: + self.logger.debug("User %s allowed in ACL: %s authorizing." + % (account_user, user_group)) + return None + + return self.denied_response(req) + + def denied_response(self, req): + """ + Returns a standard WSGI response callable with the status of 403 or 401 + depending on whether the REMOTE_USER is set or not. + """ + if req.remote_user: + self.logger.increment('forbidden') + return HTTPForbidden(request=req) + else: + return HTTPSeeOther(location=self.ext_authentication_url) + + def handle(self, env, start_response): + """ + WSGI entry point for auth requests (ones that match the + self.auth_prefix). + Wraps env in swob.Request object and passes it down. + + :param env: WSGI environment dictionary + :param start_response: WSGI callable + """ + try: + req = Request(env) + if self.auth_prefix: + req.path_info_pop() + req.bytes_transferred = '-' + req.client_disconnect = False + if 'x-storage-token' in req.headers and \ + 'x-auth-token' not in req.headers: + req.headers['x-auth-token'] = req.headers['x-storage-token'] + return self.handle_request(req)(env, start_response) + except (Exception, Timeout): + print "EXCEPTION IN handle: %s: %s" % (format_exc(), env) + self.logger.increment('errors') + start_response('500 Server Error', + [('Content-Type', 'text/plain')]) + return ['Internal server error.\n'] + + def handle_request(self, req): + """ + Entry point for auth requests (ones that match the self.auth_prefix). + Should return a WSGI-style callable (such as webob.Response). + + :param req: swob.Request object + """ + req.start_time = time() + handler = None + try: + version, account, user, _junk = req.split_path(1, 4, True) + except ValueError: + self.logger.increment('errors') + return HTTPNotFound(request=req) + if version in ('v1', 'v1.0', 'auth'): + if req.method == 'GET': + handler = self.handle_get_token + if not handler: + self.logger.increment('errors') + req.response = HTTPBadRequest(request=req) + else: + req.response = handler(req) + return req.response + + def handle_get_token(self, req): + """ + Handles the various `request for token and service end point(s)` calls. + There are various formats to support the various auth servers in the + past. Examples:: + + GET /v1//auth + GET /auth + GET /v1.0 + + All formats require GSS (Kerberos) authentication. + + On successful authentication, the response will have X-Auth-Token + set to the token to use with Swift. + + :param req: The swob.Request to process. + :returns: swob.Response, 2xx on success with data set as explained + above. + """ + # Validate the request info + try: + pathsegs = split_path(req.path_info, 1, 3, True) + except ValueError: + self.logger.increment('errors') + return HTTPNotFound(request=req) + if not ((pathsegs[0] == 'v1' and pathsegs[2] == 'auth') + or pathsegs[0] in ('auth', 'v1.0')): + return HTTPBadRequest(request=req) + + return HTTPSeeOther(location=self.ext_authentication_url) + + +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 KerbAuth(app, conf) + return auth_filter diff --git a/swiftkerbauth/swiftkerbauth.spec b/swiftkerbauth/swiftkerbauth.spec deleted file mode 100644 index f01dc8d..0000000 --- a/swiftkerbauth/swiftkerbauth.spec +++ /dev/null @@ -1,54 +0,0 @@ -Name: swiftkerbauth -Version: 1.0 -Release: 1 -Summary: Kerberos authentication filter for Swift - -Group: System Environment/Base -License: GPL -Source: %{name}.tar.gz -BuildRoot: %{_tmppath}/%{name}-root - -Requires: gluster-swift >= 1.4.8 -Requires: python-webob1.0 >= 1.0.8 - -%description -Python script which implements an authentication filter for Swift, the -object-level access layer of Red Hat Storage. - -Relies on an external authentication server, which comes with the -apachekerbauth package. - -%prep -%setup -q -n %{name} - -%build - -%install -rm -rf $RPM_BUILD_ROOT - -mkdir -p \ - $RPM_BUILD_ROOT/usr/lib/python2.6/site-packages - -install swiftkerbauth.py \ - $RPM_BUILD_ROOT/usr/lib/python2.6/site-packages - -%clean -rm -rf $RPM_BUILD_ROOT - -%files -%defattr(-,root,root,-) -/usr/lib/python2.6/site-packages/swiftkerbauth.py - -%post -if ! grep -q "filter:kerbauth" /etc/swift/proxy-server.conf; then -cat >>/etc/swift/proxy-server.conf < - 1.0-1 -- initial build diff --git a/swiftkerbauth/swiftkerbauth/swiftkerbauth.py b/swiftkerbauth/swiftkerbauth/swiftkerbauth.py deleted file mode 100644 index ca9cfc7..0000000 --- a/swiftkerbauth/swiftkerbauth/swiftkerbauth.py +++ /dev/null @@ -1,345 +0,0 @@ -# Copyright (c) 2013 Red Hat, Inc. -# -# 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 time import gmtime, strftime, time -from traceback import format_exc -from urllib import quote, unquote - -from eventlet import Timeout - -from pkg_resources import require -require("WebOb>=1.0.8") - -from webob import Response, Request -from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPNotFound, \ - HTTPSeeOther - -from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed -from swift.common.utils import cache_from_env, get_logger, get_remote_client, \ - split_path, TRUE_VALUES - -class KerbAuth(object): - """ - Test authentication and authorization system. - - Add to your pipeline in proxy-server.conf, such as:: - - [pipeline:main] - pipeline = catch_errors cache kerbauth proxy-server - - Set account auto creation to true in proxy-server.conf:: - - [app:proxy-server] - account_autocreate = true - - And add a kerbauth filter section, such as:: - - [filter:kerbauth] - use = egg:swift#kerbauth - - See the proxy-server.conf-sample for more information. - - :param app: The next WSGI app in the pipeline - :param conf: The dict of configuration values - """ - - def __init__(self, app, conf): - self.app = app - self.conf = conf - self.logger = get_logger(conf, log_route='kerbauth') - self.log_headers = conf.get('log_headers') == 'True' - self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip() - if self.reseller_prefix and self.reseller_prefix[-1] != '_': - self.reseller_prefix += '_' - self.auth_prefix = conf.get('auth_prefix', '/auth/') - if not self.auth_prefix: - self.auth_prefix = '/auth/' - if self.auth_prefix[0] != '/': - self.auth_prefix = '/' + self.auth_prefix - if self.auth_prefix[-1] != '/': - self.auth_prefix += '/' - self.token_life = int(conf.get('token_life', 86400)) - self.allowed_sync_hosts = [h.strip() - for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',') - if h.strip()] - self.allow_overrides = \ - conf.get('allow_overrides', 't').lower() in TRUE_VALUES - self.ext_authentication_url = conf.get('ext_authentication_url') - if not self.ext_authentication_url: - raise RuntimeError("Missing filter parameter ext_authentication_url in /etc/swift/proxy-server.conf") - - def __call__(self, env, start_response): - """ - Accepts a standard WSGI application call, authenticating the request - and installing callback hooks for authorization and ACL header - validation. For an authenticated request, REMOTE_USER will be set to a - comma separated list of the user's groups. - - If the request matches the self.auth_prefix, the request will be - routed through the internal auth request handler (self.handle). - This is to handle granting tokens, etc. - """ - if self.allow_overrides and env.get('swift.authorize_override', False): - return self.app(env, start_response) - if env.get('PATH_INFO', '').startswith(self.auth_prefix): - return self.handle(env, start_response) - token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) - if token and token.startswith(self.reseller_prefix): - groups = self.get_groups(env, token) - if groups: - env['REMOTE_USER'] = groups - user = groups and groups.split(',', 1)[0] or '' - # We know the proxy logs the token, so we augment it just a bit - # to also log the authenticated user. - env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token) - env['swift.authorize'] = self.authorize - env['swift.clean_acl'] = clean_acl - else: - # Invalid token (may be expired) - return HTTPSeeOther(location=self.ext_authentication_url)(env, start_response) - else: - # With a non-empty reseller_prefix, I would like to be called - # back for anonymous access to accounts I know I'm the - # definitive auth for. - try: - version, rest = split_path(env.get('PATH_INFO', ''), - 1, 2, True) - except ValueError: - return HTTPNotFound()(env, start_response) - # Not my token, not my account, I can't authorize this request, - # deny all is a good idea if not already set... - if 'swift.authorize' not in env: - env['swift.authorize'] = self.denied_response - - return self.app(env, start_response) - - def get_groups(self, env, token): - """ - Get groups for the given token. - - :param env: The current WSGI environment dictionary. - :param token: Token to validate and return a group string for. - - :returns: None if the token is invalid or a string containing a comma - separated list of groups the authenticated user is a member - of. The first group in the list is also considered a unique - identifier for that user. - """ - groups = None - memcache_client = cache_from_env(env) - if not memcache_client: - raise Exception('Memcache required') - memcache_token_key = '%s/token/%s' % (self.reseller_prefix, token) - cached_auth_data = memcache_client.get(memcache_token_key) - if cached_auth_data: - expires, groups = cached_auth_data - if expires < time(): - groups = None - - return groups - - def authorize(self, req): - """ - Returns None if the request is authorized to continue or a standard - WSGI response callable if not. - - Assumes that user groups are all lower case, which is true when Red Hat - Enterprise Linux Identity Management is used. - """ - try: - version, account, container, obj = split_path(req.path, 1, 4, True) - except ValueError: - return HTTPNotFound(request=req) - if not account or not account.startswith(self.reseller_prefix): - return self.denied_response(req) - user_groups = (req.remote_user or '').split(',') - # If the user is in the reseller_admin group for our prefix, he gets - # full access to all accounts we manage. For the default reseller - # prefix, the group name is auth_reseller_admin. - admin_group = ("%sreseller_admin" % self.reseller_prefix).lower() - if admin_group in user_groups and \ - account != self.reseller_prefix and \ - account[len(self.reseller_prefix)] != '.': - req.environ['swift_owner'] = True - return None - # The "account" is part of the request URL, and already contains the - # reseller prefix, like in "/v1/AUTH_vol1/pictures/pic1.png". - if account.lower() in user_groups and \ - (req.method not in ('DELETE', 'PUT') or container): - # If the user is admin for the account and is not trying to do an - # account DELETE or PUT... - req.environ['swift_owner'] = True - return None - if (req.environ.get('swift_sync_key') and - req.environ['swift_sync_key'] == - req.headers.get('x-container-sync-key', None) and - 'x-timestamp' in req.headers and - (req.remote_addr in self.allowed_sync_hosts or - get_remote_client(req) in self.allowed_sync_hosts)): - return None - referrers, groups = parse_acl(getattr(req, 'acl', None)) - if referrer_allowed(req.referer, referrers): - if obj or '.rlistings' in groups: - return None - return self.denied_response(req) - if not req.remote_user: - return self.denied_response(req) - for user_group in user_groups: - if user_group in groups: - return None - return self.denied_response(req) - - def denied_response(self, req): - """ - Returns a standard WSGI response callable with the status of 403 or 401 - depending on whether the REMOTE_USER is set or not. - """ - if req.remote_user: - return HTTPForbidden(request=req) - else: - return HTTPSeeOther(location=self.ext_authentication_url) - - def handle(self, env, start_response): - """ - WSGI entry point for auth requests (ones that match the - self.auth_prefix). - Wraps env in webob.Request object and passes it down. - - :param env: WSGI environment dictionary - :param start_response: WSGI callable - """ - try: - req = Request(env) - if self.auth_prefix: - req.path_info_pop() - req.bytes_transferred = '-' - req.client_disconnect = False - if 'x-storage-token' in req.headers and \ - 'x-auth-token' not in req.headers: - req.headers['x-auth-token'] = req.headers['x-storage-token'] - if 'eventlet.posthooks' in env: - env['eventlet.posthooks'].append( - (self.posthooklogger, (req,), {})) - return self.handle_request(req)(env, start_response) - else: - # Lack of posthook support means that we have to log on the - # start of the response, rather than after all the data has - # been sent. This prevents logging client disconnects - # differently than full transmissions. - response = self.handle_request(req)(env, start_response) - self.posthooklogger(env, req) - return response - except (Exception, Timeout): - print "EXCEPTION IN handle: %s: %s" % (format_exc(), env) - start_response('500 Server Error', - [('Content-Type', 'text/plain')]) - return ['Internal server error.\n'] - - def handle_request(self, req): - """ - Entry point for auth requests (ones that match the self.auth_prefix). - Should return a WSGI-style callable (such as webob.Response). - - :param req: webob.Request object - """ - req.start_time = time() - handler = None - try: - version, account, user, _junk = split_path(req.path_info, - minsegs=1, maxsegs=4, rest_with_last=True) - except ValueError: - return HTTPNotFound(request=req) - if version in ('v1', 'v1.0', 'auth'): - if req.method == 'GET': - handler = self.handle_get_token - if not handler: - req.response = HTTPBadRequest(request=req) - else: - req.response = handler(req) - return req.response - - def handle_get_token(self, req): - """ - Handles the various `request for token and service end point(s)` calls. - There are various formats to support the various auth servers in the - past. Examples:: - - GET /v1//auth - GET /auth - GET /v1.0 - - All formats require GSS (Kerberos) authentication. - - On successful authentication, the response will have X-Auth-Token - set to the token to use with Swift. - - :param req: The webob.Request to process. - :returns: webob.Response, 2xx on success with data set as explained - above. - """ - # Validate the request info - try: - pathsegs = split_path(req.path_info, minsegs=1, maxsegs=3, - rest_with_last=True) - except ValueError: - return HTTPNotFound(request=req) - if not ((pathsegs[0] == 'v1' and pathsegs[2] == 'auth') or pathsegs[0] in ('auth', 'v1.0')): - return HTTPBadRequest(request=req) - - return HTTPSeeOther(location=self.ext_authentication_url) - - def posthooklogger(self, env, req): - if not req.path.startswith(self.auth_prefix): - return - response = getattr(req, 'response', None) - if not response: - return - trans_time = '%.4f' % (time() - req.start_time) - the_request = quote(unquote(req.path)) - if req.query_string: - the_request = the_request + '?' + req.query_string - # remote user for zeus - client = req.headers.get('x-cluster-client-ip') - if not client and 'x-forwarded-for' in req.headers: - # remote user for other lbs - client = req.headers['x-forwarded-for'].split(',')[0].strip() - logged_headers = None - if self.log_headers: - logged_headers = '\n'.join('%s: %s' % (k, v) - for k, v in req.headers.items()) - status_int = response.status_int - if getattr(req, 'client_disconnect', False) or \ - getattr(response, 'client_disconnect', False): - status_int = 499 - self.logger.info(' '.join(quote(str(x)) for x in (client or '-', - req.remote_addr or '-', strftime('%d/%b/%Y/%H/%M/%S', gmtime()), - req.method, the_request, req.environ['SERVER_PROTOCOL'], - status_int, req.referer or '-', req.user_agent or '-', - req.headers.get('x-auth-token', - req.headers.get('x-auth-admin-user', '-')), - getattr(req, 'bytes_transferred', 0) or '-', - getattr(response, 'bytes_transferred', 0) or '-', - req.headers.get('etag', '-'), - req.environ.get('swift.trans_id', '-'), logged_headers or '-', - trans_time))) - - -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 KerbAuth(app, conf) - return auth_filter -- cgit