summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--MANIFEST.in2
-rwxr-xr-xapachekerbauth/var/www/cgi-bin/swift-auth1
-rwxr-xr-xmakerpm.sh8
-rw-r--r--setup.py44
-rw-r--r--test-requirements.txt12
-rw-r--r--test/__init__.py19
-rw-r--r--test/unit/__init__.py0
-rw-r--r--test/unit/test_kerbauth.py368
-rw-r--r--tox.ini38
-rwxr-xr-xunittests.sh25
11 files changed, 504 insertions, 19 deletions
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
--- /dev/null
+++ b/test/unit/__init__.py
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
+