From 9812a4a9e4a30a208d77d3b10828a1c174dccd77 Mon Sep 17 00:00:00 2001 From: Prashanth Pai Date: Thu, 10 Oct 2013 15:47:31 +0530 Subject: Add unit tests Change-Id: I7bbf74b66c26d0a964fa769bf9c46dd73bd03d73 Signed-off-by: Prashanth Pai Reviewed-on: http://review.gluster.org/6067 Reviewed-by: Luis Pabon Tested-by: Luis Pabon --- .gitignore | 6 +- MANIFEST.in | 2 +- apachekerbauth/var/www/cgi-bin/swift-auth | 1 + makerpm.sh | 8 - setup.py | 44 +++- test-requirements.txt | 12 + test/__init__.py | 19 ++ test/unit/__init__.py | 0 test/unit/test_kerbauth.py | 368 ++++++++++++++++++++++++++++++ tox.ini | 38 +++ unittests.sh | 25 ++ 11 files changed, 504 insertions(+), 19 deletions(-) delete mode 100755 makerpm.sh create mode 100644 test-requirements.txt create mode 100644 test/__init__.py create mode 100644 test/unit/__init__.py create mode 100644 test/unit/test_kerbauth.py create mode 100644 tox.ini create mode 100755 unittests.sh diff --git a/.gitignore b/.gitignore index 835fe8b..cbc00c6 100644 --- a/.gitignore +++ b/.gitignore @@ -21,9 +21,11 @@ lib64 pip-log.txt # Unit test / coverage reports -.coverage .tox -nosetests.xml +test/unit/.coverage +test/unit/nosetests.xml +test/unit/coverage.xml +test/unit/cover # Translations *.mo diff --git a/MANIFEST.in b/MANIFEST.in index cadec55..67cbf75 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ include LICENSE README -recursive-include swiftkerbauth *.py +recursive-include swiftkerbauth test *.py graft doc graft apachekerbauth diff --git a/apachekerbauth/var/www/cgi-bin/swift-auth b/apachekerbauth/var/www/cgi-bin/swift-auth index 6173408..1d124c5 100755 --- a/apachekerbauth/var/www/cgi-bin/swift-auth +++ b/apachekerbauth/var/www/cgi-bin/swift-auth @@ -40,6 +40,7 @@ MEMCACHE_SERVERS = ['127.0.0.1:11211'] DEBUG_HEADERS = True + def main(): remote_user = os.environ['REMOTE_USER'] matches = re.match('([^@]+)@.*', remote_user) diff --git a/makerpm.sh b/makerpm.sh deleted file mode 100755 index 9635e14..0000000 --- a/makerpm.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/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 index ce03189..01210df 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,37 @@ #!/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. + from setuptools import setup from swiftkerbauth import __version__ +import os + + +# Ugly hack to exclude data_files if running in tox as non root +def include_data_files(): + data = [ + ('/var/www/cgi-bin', + ['apachekerbauth/var/www/cgi-bin/swift-auth']), + ('/etc/httpd/conf.d', + ['apachekerbauth/etc/httpd/conf.d/swift-auth.conf']), + ] + if os.geteuid() != 0: + data = None + return data + setup( name='swiftkerbauth', @@ -14,6 +44,7 @@ setup( packages=['swiftkerbauth'], keywords='openstack swift kerberos', install_requires=['swift>=1.9.1'], + test_suite='nose.collector', classifiers=[ 'Development Status :: 3 - Alpha', 'Environment :: OpenStack', @@ -25,14 +56,11 @@ setup( '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']), - ], + ], + data_files=include_data_files(), entry_points={ 'paste.filter_factory': [ 'kerbauth=swiftkerbauth.kerbauth:filter_factory', - ], - }, - ) + ], + }, +) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..fdebbb6 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,12 @@ +# Install bounded pep8/pyflakes first, then let flake8 install +pep8==1.4.5 +pyflakes==0.7.2 +flake8==2.0 +hacking>=0.5.6,<0.6 +coverage +nose +nosexcover +openstack.nose_plugin +nosehtmloutput +sphinx>=1.1.2 +mock>=0.8.0 diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..3e44252 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,19 @@ +# 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. + +# See http://code.google.com/p/python-nose/issues/detail?id=373 +# The code below enables nosetests to work with i18n _() blocks + +import __builtin__ +setattr(__builtin__, '_', lambda x: x) diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/test_kerbauth.py b/test/unit/test_kerbauth.py new file mode 100644 index 0000000..95697a4 --- /dev/null +++ b/test/unit/test_kerbauth.py @@ -0,0 +1,368 @@ +# 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. + +import unittest +from contextlib import contextmanager +from time import time + +from swiftkerbauth import kerbauth as auth +from swift.common.swob import Request, Response + +EXT_AUTHENTICATION_URL = "127.0.0.1" +REDIRECT_STATUS = 302 + + +def my_filter_factory(global_conf, **local_conf): + if 'ext_authentication_url' not in global_conf: + global_conf['ext_authentication_url'] = EXT_AUTHENTICATION_URL + conf = global_conf.copy() + conf.update(local_conf) + + def auth_filter(app): + return auth.KerbAuth(app, conf) + return auth_filter + +# Monkey patching filter_factory to always pass ext_authentication_url +# as a parameter. Absence of ext_authentication_url raises a RuntimeError + + +def patch_filter_factory(): + auth.filter_factory = my_filter_factory + + +def unpatch_filter_factory(): + reload(auth) + + +class FakeMemcache(object): + + def __init__(self): + self.store = {} + + def get(self, key): + return self.store.get(key) + + def set(self, key, value, time=0): + self.store[key] = value + return True + + def incr(self, key, time=0): + self.store[key] = self.store.setdefault(key, 0) + 1 + return self.store[key] + + @contextmanager + def soft_lock(self, key, timeout=0, retries=5): + yield True + + def delete(self, key): + try: + del self.store[key] + except Exception: + pass + return True + + +class FakeApp(object): + + def __init__(self, status_headers_body_iter=None, acl=None, sync_key=None): + self.calls = 0 + self.status_headers_body_iter = status_headers_body_iter + if not self.status_headers_body_iter: + self.status_headers_body_iter = iter([('404 Not Found', {}, '')]) + self.acl = acl + self.sync_key = sync_key + + def __call__(self, env, start_response): + self.calls += 1 + self.request = Request.blank('', environ=env) + if self.acl: + self.request.acl = self.acl + if self.sync_key: + self.request.environ['swift_sync_key'] = self.sync_key + if 'swift.authorize' in env: + resp = env['swift.authorize'](self.request) + if resp: + return resp(env, start_response) + status, headers, body = self.status_headers_body_iter.next() + return Response(status=status, headers=headers, + body=body)(env, start_response) + + +class TestAuth(unittest.TestCase): + + # Patch auth.filter_factory() + patch_filter_factory() + + def setUp(self): + self.test_auth = auth.filter_factory({})(FakeApp()) + + def _make_request(self, path, **kwargs): + req = Request.blank(path, **kwargs) + req.environ['swift.cache'] = FakeMemcache() + return req + + def test_no_ext_authentication_url(self): + app = FakeApp() + try: + # Use original auth.filter_factory and NOT monkey patched version + unpatch_filter_factory() + auth.filter_factory({})(app) + except RuntimeError as e: + # Restore monkey patched version + patch_filter_factory() + self.assertTrue(e.args[0].startswith("Missing filter parameter " + "ext_authentication_url")) + + def test_reseller_prefix_init(self): + app = FakeApp() + ath = auth.filter_factory({})(app) + self.assertEquals(ath.reseller_prefix, 'AUTH_') + ath = auth.filter_factory({'reseller_prefix': 'TEST'})(app) + self.assertEquals(ath.reseller_prefix, 'TEST_') + ath = auth.filter_factory({'reseller_prefix': 'TEST_'})(app) + self.assertEquals(ath.reseller_prefix, 'TEST_') + + def test_auth_prefix_init(self): + app = FakeApp() + ath = auth.filter_factory({})(app) + self.assertEquals(ath.auth_prefix, '/auth/') + ath = auth.filter_factory({'auth_prefix': ''})(app) + self.assertEquals(ath.auth_prefix, '/auth/') + ath = auth.filter_factory({'auth_prefix': '/'})(app) + self.assertEquals(ath.auth_prefix, '/auth/') + ath = auth.filter_factory({'auth_prefix': '/test/'})(app) + self.assertEquals(ath.auth_prefix, '/test/') + ath = auth.filter_factory({'auth_prefix': '/test'})(app) + self.assertEquals(ath.auth_prefix, '/test/') + ath = auth.filter_factory({'auth_prefix': 'test/'})(app) + self.assertEquals(ath.auth_prefix, '/test/') + ath = auth.filter_factory({'auth_prefix': 'test'})(app) + self.assertEquals(ath.auth_prefix, '/test/') + + def test_top_level_redirect(self): + req = self._make_request('/') + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, REDIRECT_STATUS) + self.assertEquals(req.environ['swift.authorize'], + self.test_auth.denied_response) + + def test_override_asked_for_and_allowed(self): + self.test_auth = \ + auth.filter_factory({'allow_overrides': 'true'})(FakeApp()) + req = self._make_request('/v1/AUTH_account', + environ={'swift.authorize_override': True}) + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 404) + self.assertTrue('swift.authorize' not in req.environ) + + def test_override_default_allowed(self): + req = self._make_request('/v1/AUTH_account', + environ={'swift.authorize_override': True}) + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 404) + self.assertTrue('swift.authorize' not in req.environ) + + def test_options_call(self): + req = self._make_request('/v1/AUTH_cfa/c/o', + environ={'REQUEST_METHOD': 'OPTIONS'}) + resp = self.test_auth.authorize(req) + self.assertEquals(resp, None) + + def test_auth_deny_non_reseller_prefix_no_override(self): + fake_authorize = lambda x: Response(status='500 Fake') + req = self._make_request('/v1/BLAH_account', + headers={'X-Auth-Token': 'BLAH_t'}, + environ={'swift.authorize': fake_authorize} + ) + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, 500) + self.assertEquals(req.environ['swift.authorize'], fake_authorize) + + def test_authorize_acl_group_access(self): + req = self._make_request('/v1/AUTH_cfa') + req.remote_user = 'act:usr,act' + resp = self.test_auth.authorize(req) + self.assertEquals(resp.status_int, 403) + req = self._make_request('/v1/AUTH_cfa') + req.remote_user = 'act:usr,act' + req.acl = 'act' + self.assertEquals(self.test_auth.authorize(req), None) + req = self._make_request('/v1/AUTH_cfa') + req.remote_user = 'act:usr,act' + req.acl = 'act:usr' + self.assertEquals(self.test_auth.authorize(req), None) + req = self._make_request('/v1/AUTH_cfa') + req.remote_user = 'act:usr,act' + + def test_deny_cross_reseller(self): + # Tests that cross-reseller is denied, even if ACLs/group names match + req = self._make_request('/v1/OTHER_cfa') + req.remote_user = 'act:usr,act,AUTH_cfa' + req.acl = 'act' + resp = self.test_auth.authorize(req) + self.assertEquals(resp.status_int, 403) + + def test_authorize_acl_referer_after_user_groups(self): + req = self._make_request('/v1/AUTH_cfa/c') + req.remote_user = 'act:usr' + req.acl = '.r:*,act:usr' + self.assertEquals(self.test_auth.authorize(req), None) + + def test_detect_reseller_request(self): + req = self._make_request('/v1/AUTH_admin', + headers={'X-Auth-Token': 'AUTH_t'}) + cache_key = 'AUTH_/token/AUTH_t' + cache_entry = (time() + 3600, '.reseller_admin') + req.environ['swift.cache'].set(cache_key, cache_entry) + req.get_response(self.test_auth) + self.assertTrue(req.environ.get('reseller_request', False)) + + def test_regular_is_not_owner(self): + orig_authorize = self.test_auth.authorize + owner_values = [] + + def mitm_authorize(req): + rv = orig_authorize(req) + owner_values.append(req.environ.get('swift_owner', False)) + return rv + + self.test_auth.authorize = mitm_authorize + + req = self._make_request( + '/v1/AUTH_cfa/c', + headers={'X-Auth-Token': 'AUTH_t'}) + req.remote_user = 'act:usr' + self.test_auth.authorize(req) + self.assertEquals(owner_values, [False]) + + def test_no_memcache(self): + env = {'swift.cache': None} + try: + self.test_auth.get_groups(env, None) + except Exception as e: + self.assertTrue(e.args[0].startswith("Memcache required")) + + def test_handle_request(self): + req = self._make_request('/auth/v1.0') + resp = self.test_auth.handle_request(req) + self.assertEquals(resp.status_int, REDIRECT_STATUS) + + def test_handle_request_bad_request(self): + req = self._make_request('////') + resp = self.test_auth.handle_request(req) + self.assertEquals(resp.status_int, 404) + + def test_handle_request_no_handler(self): + req = self._make_request('/blah/blah/blah/blah') + resp = self.test_auth.handle_request(req) + self.assertEquals(resp.status_int, 400) + + def test_handle_get_token_bad_request(self): + req = self._make_request('/blah/blah') + resp = self.test_auth.handle_get_token(req) + self.assertEquals(resp.status_int, 400) + req = self._make_request('/////') + resp = self.test_auth.handle_get_token(req) + self.assertEquals(resp.status_int, 404) + + def test_handle(self): + req = self._make_request('/auth/v1.0') + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, REDIRECT_STATUS) + + def test_authorize_invalid_req(self): + req = self._make_request('/') + resp = self.test_auth.authorize(req) + self.assertEquals(resp.status_int, 404) + + def test_authorize_set_swift_owner(self): + req = self._make_request('/v1/AUTH_test/c1/o1') + req.remote_user = 'test,auth_reseller_admin' + resp = self.test_auth.authorize(req) + self.assertEquals(req.environ['swift_owner'], True) + self.assertTrue(resp is None) + req = self._make_request('/v1/AUTH_test/c1/o1') + req.remote_user = 'test,auth_test' + resp = self.test_auth.authorize(req) + self.assertEquals(req.environ['swift_owner'], True) + self.assertTrue(resp is None) + + def test_authorize_swift_sync_key(self): + req = self._make_request( + '/v1/AUTH_cfa/c/o', + environ={'swift_sync_key': 'secret'}, + headers={'x-container-sync-key': 'secret', + 'x-timestamp': '123.456'}) + resp = self.test_auth.authorize(req) + self.assertTrue(resp is None) + + def test_authorize_acl_referrer_access(self): + req = self._make_request('/v1/AUTH_cfa/c') + req.remote_user = 'act:usr,act' + resp = self.test_auth.authorize(req) + self.assertEquals(resp.status_int, 403) + req = self._make_request('/v1/AUTH_cfa/c') + req.remote_user = 'act:usr,act' + req.acl = '.r:*,.rlistings' + self.assertEquals(self.test_auth.authorize(req), None) + req = self._make_request('/v1/AUTH_cfa/c') + req.remote_user = 'act:usr,act' + req.acl = '.r:*' # No listings allowed + resp = self.test_auth.authorize(req) + self.assertEquals(resp.status_int, 403) + req = self._make_request('/v1/AUTH_cfa/c') + req.remote_user = 'act:usr,act' + req.acl = '.r:.example.com,.rlistings' + resp = self.test_auth.authorize(req) + self.assertEquals(resp.status_int, 403) + req = self._make_request('/v1/AUTH_cfa/c') + req.remote_user = 'act:usr,act' + req.referer = 'http://www.example.com/index.html' + req.acl = '.r:.example.com,.rlistings' + self.assertEquals(self.test_auth.authorize(req), None) + req = self._make_request('/v1/AUTH_cfa/c') + resp = self.test_auth.authorize(req) + self.assertEquals(resp.status_int, REDIRECT_STATUS) + req = self._make_request('/v1/AUTH_cfa/c') + req.acl = '.r:*,.rlistings' + self.assertEquals(self.test_auth.authorize(req), None) + req = self._make_request('/v1/AUTH_cfa/c') + req.acl = '.r:*' # No listings allowed + resp = self.test_auth.authorize(req) + self.assertEquals(resp.status_int, REDIRECT_STATUS) + req = self._make_request('/v1/AUTH_cfa/c') + req.acl = '.r:.example.com,.rlistings' + resp = self.test_auth.authorize(req) + self.assertEquals(resp.status_int, REDIRECT_STATUS) + req = self._make_request('/v1/AUTH_cfa/c') + req.referer = 'http://www.example.com/index.html' + req.acl = '.r:.example.com,.rlistings' + self.assertEquals(self.test_auth.authorize(req), None) + + def test_handle_x_storage_token(self): + req = self._make_request( + '/auth/v1.0', + headers={'x-storage-token': 'blahblah', }) + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, REDIRECT_STATUS) + + def test_invalid_token(self): + req = self._make_request('/k1/test') + req.environ['HTTP_X_AUTH_TOKEN'] = 'AUTH_blahblahblah' + resp = req.get_response(self.test_auth) + self.assertEquals(resp.status_int, REDIRECT_STATUS) + +if __name__ == '__main__': + unittest.main() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..c06ab81 --- /dev/null +++ b/tox.ini @@ -0,0 +1,38 @@ +[tox] +envlist = py26,py27,pep8 + +[testenv] +setenv = VIRTUAL_ENV={envdir} + NOSE_WITH_OPENSTACK=1 + NOSE_OPENSTACK_COLOR=1 + NOSE_OPENSTACK_RED=0.05 + NOSE_OPENSTACK_YELLOW=0.025 + NOSE_OPENSTACK_SHOW_ELAPSED=1 + NOSE_OPENSTACK_STDOUT=1 +deps = + --download-cache={homedir}/.pipcache + https://launchpad.net/swift/havana/1.9.1/+download/swift-1.9.1.tar.gz + -r{toxinidir}/test-requirements.txt +changedir = {toxinidir}/test/unit +commands = nosetests -v --exe --with-xunit --with-coverage --cover-package swiftkerbauth --cover-erase --cover-xml --cover-html --cover-branches {posargs} + +[tox:jenkins] +downloadcache = ~/cache/pip + +[testenv:pep8] +changedir = {toxinidir} +commands = + flake8 + flake8 apachekerbauth/var/www/cgi-bin/swift-auth + +[testenv:cover] +setenv = NOSE_WITH_COVERAGE=1 + +[testenv:venv] +commands = {posargs} + +[flake8] +ignore = H +builtins = _ +exclude = .venv,.tox,dist,doc,*egg +show-source = True diff --git a/unittests.sh b/unittests.sh new file mode 100755 index 0000000..dc5927c --- /dev/null +++ b/unittests.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# 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. + + +cd $(dirname $0)/test/unit +nosetests -v --exe --with-coverage --cover-package swiftkerbauth --cover-erase --cover-html --cover-branches $@ + +saved_status=$? +rm -f .coverage +exit $saved_status + -- cgit