From 991989bc04178442b2a6b766a67f7a26e60c08f0 Mon Sep 17 00:00:00 2001 From: Prashanth Pai Date: Wed, 6 Nov 2013 17:30:28 +0530 Subject: Modularize swift-auth CGI script, add unit tests - Moved most of swift-auth CGI script to kerbauth_utils.py - Added unit tests for kerbauth_utils.py - Made MEMCACHE_SERVERS, DEBUG_HEADERS, TOKEN_LIFE as configurable parameters Change-Id: I2e9e9823e8aa99dc2cf41327c55428350c8768dc Signed-off-by: Prashanth Pai Reviewed-on: http://review.gluster.org/6248 Tested-by: Chetan Risbud Reviewed-by: Chetan Risbud Reviewed-by: Luis Pabon Tested-by: Luis Pabon --- apachekerbauth/var/www/cgi-bin/swift-auth | 96 ++++++--------------------- doc/architecture.md | 7 +- setup.py | 7 +- swiftkerbauth/__init__.py | 21 +++++- swiftkerbauth/kerbauth.py | 8 +-- swiftkerbauth/kerbauth_utils.py | 106 ++++++++++++++++++++++++++++++ test/unit/__init__.py | 44 +++++++++++++ test/unit/test_kerbauth.py | 30 +-------- test/unit/test_kerbauth_utils.py | 77 ++++++++++++++++++++++ 9 files changed, 278 insertions(+), 118 deletions(-) create mode 100644 swiftkerbauth/kerbauth_utils.py create mode 100644 test/unit/test_kerbauth_utils.py diff --git a/apachekerbauth/var/www/cgi-bin/swift-auth b/apachekerbauth/var/www/cgi-bin/swift-auth index 1d124c5..45df45c 100755 --- a/apachekerbauth/var/www/cgi-bin/swift-auth +++ b/apachekerbauth/var/www/cgi-bin/swift-auth @@ -18,95 +18,41 @@ # setsebool -P httpd_can_network_connect 1 # setsebool -P httpd_can_network_memcache 1 +import os 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 +from swiftkerbauth import MEMCACHE_SERVERS, TOKEN_LIFE, DEBUG_HEADERS +from swiftkerbauth.kerbauth_utils import get_remote_user, get_auth_data, \ + generate_token, set_auth_data, get_groups def main(): - remote_user = os.environ['REMOTE_USER'] - matches = re.match('([^@]+)@.*', remote_user) - if not matches: - raise RuntimeError("Malformed REMOTE_USER \"%s\"" % remote_user) + try: + username = get_remote_user(os.environ) + except RuntimeError: + print "Status: 401 Unauthorized\n" + print "Malformed REMOTE_USER" + return - username = matches.group(1) + if not MEMCACHE_SERVERS: + print "Status: 500 Internal Server Error\n" + print "Memcache not configured in /etc/swift/proxy-server.conf" + return - mc = MemcacheRing(MEMCACHE_SERVERS) + mc_servers = [s.strip() for s in MEMCACHE_SERVERS.split(',') if s.strip()] + mc = MemcacheRing(mc_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 + token, expires, groups = get_auth_data(mc, username) 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) - + token = generate_token() 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) + groups = get_groups(username) + set_auth_data(mc, username, token, expires, groups) print "X-Auth-Token: %s" % token + print "X-Storage-Token: %s" % token # For debugging. if DEBUG_HEADERS: diff --git a/doc/architecture.md b/doc/architecture.md index cfe64d2..fc6d764 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -38,10 +38,9 @@ either grants or denies the resource access. ## kerbauth.py -The script /usr/lib/python2.6/site-packages/swiftkerbauth/kerbauth.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: +The script kerbauth.py began as a copy of the tempauth.py script from +from tempauth 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 diff --git a/setup.py b/setup.py index 65aab59..d435a39 100644 --- a/setup.py +++ b/setup.py @@ -16,9 +16,10 @@ # limitations under the License. from setuptools import setup -from swiftkerbauth import __version__ import os +__version__ = '1.0.0' + with open('README.md') as file: long_description = file.read() @@ -44,10 +45,10 @@ setup( author='Red Hat, Inc.', author_email='gluster-users@gluster.org', long_description=long_description, - url='https://forge.gluster.org/swiftkerbauth', + url='https://github.com/gluster/swiftkrbauth/', packages=['swiftkerbauth'], keywords='openstack swift kerberos', - install_requires=['swift>=1.9.1'], + install_requires=['swift>=1.10.0'], test_suite='nose.collector', classifiers=[ 'Development Status :: 3 - Alpha', diff --git a/swiftkerbauth/__init__.py b/swiftkerbauth/__init__.py index eaa1d88..abdbeaa 100644 --- a/swiftkerbauth/__init__.py +++ b/swiftkerbauth/__init__.py @@ -14,4 +14,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.0.0" + +from swift.common.utils import readconf, config_true_value + +config_file = {} +try: + config_file = readconf("/etc/swift/proxy-server.conf", + section_name="filter:cache") +except SystemExit: + pass +MEMCACHE_SERVERS = config_file.get('memcache_servers', None) + +config_file = {} +try: + config_file = readconf("/etc/swift/proxy-server.conf", + section_name="filter:kerbauth") +except SystemExit: + pass +TOKEN_LIFE = int(config_file.get('token_life', 86400)) +RESELLER_PREFIX = config_file.get('reseller_prefix', "AUTH_") +DEBUG_HEADERS = config_true_value(config_file.get('debug_headers', 'yes')) diff --git a/swiftkerbauth/kerbauth.py b/swiftkerbauth/kerbauth.py index 612299d..a1ba091 100644 --- a/swiftkerbauth/kerbauth.py +++ b/swiftkerbauth/kerbauth.py @@ -17,12 +17,8 @@ from traceback import format_exc from eventlet import Timeout from swift.common.swob import Request -from swift.common.swob import HTTPBadRequest, HTTPForbidden, HTTPNotFound - -try: - from swift.common.swob import HTTPSeeOther -except ImportError: - from swift.common.swob import HTTPFound as HTTPSeeOther +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, \ diff --git a/swiftkerbauth/kerbauth_utils.py b/swiftkerbauth/kerbauth_utils.py new file mode 100644 index 0000000..507580e --- /dev/null +++ b/swiftkerbauth/kerbauth_utils.py @@ -0,0 +1,106 @@ +# 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 re +import random +import grp +import subprocess +from time import time +from swiftkerbauth import TOKEN_LIFE, RESELLER_PREFIX + + +def get_remote_user(env): + """Retrieve REMOTE_USER set by Apache from environment.""" + remote_user = env.get('REMOTE_USER', "") + matches = re.match('([^@]+)@.*', remote_user) + if not matches: + raise RuntimeError("Malformed REMOTE_USER \"%s\"" % remote_user) + return matches.group(1) + + +def get_auth_data(mc, username): + """ + Returns the token, expiry time and groups for the user if it already exists + on memcache. Returns None otherwise. + + :param mc: MemcacheRing object + :param username: swift user + """ + token, expires, groups = None, None, None + memcache_user_key = '%s/user/%s' % (RESELLER_PREFIX, username) + 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 + else: + expires, groups = None, None + return (token, expires, groups) + + +def set_auth_data(mc, username, token, expires, groups): + """ + Stores the following key value pairs on Memcache: + (token, expires+groups) + (user, token) + """ + 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) + + +def generate_token(): + """Generates a random 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))) + return token + + +def get_groups(username): + """Return a set of groups to which the user belongs to.""" + # 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" % username) + (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) + return groups diff --git a/test/unit/__init__.py b/test/unit/__init__.py index e69de29..3e26378 100644 --- a/test/unit/__init__.py +++ b/test/unit/__init__.py @@ -0,0 +1,44 @@ +# 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 contextlib import contextmanager + + +class FakeMemcache(object): + """A Fake class to emulate memcache.""" + + def __init__(self): + self.store = {} + + def get(self, key): + return self.store.get(key) + + def set(self, key, value, timeout=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 diff --git a/test/unit/test_kerbauth.py b/test/unit/test_kerbauth.py index 446abb8..1771314 100644 --- a/test/unit/test_kerbauth.py +++ b/test/unit/test_kerbauth.py @@ -14,10 +14,10 @@ # limitations under the License. import unittest -from contextlib import contextmanager from time import time from swiftkerbauth import kerbauth as auth +from test.unit import FakeMemcache from swift.common.swob import Request, Response EXT_AUTHENTICATION_URL = "127.0.0.1" @@ -46,34 +46,6 @@ 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): diff --git a/test/unit/test_kerbauth_utils.py b/test/unit/test_kerbauth_utils.py new file mode 100644 index 0000000..abf8ad0 --- /dev/null +++ b/test/unit/test_kerbauth_utils.py @@ -0,0 +1,77 @@ +# 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 +import re +from time import time +from test.unit import FakeMemcache +from swiftkerbauth import kerbauth_utils as ku + + +class TestKerbUtils(unittest.TestCase): + + def test_get_remote_user(self): + env = {'REMOTE_USER': "auth_admin@EXAMPLE.COM"} + result = ku.get_remote_user(env) + self.assertEqual(result, "auth_admin") + + def test_get_remote_user_err(self): + env = {'REMOTE_USER': "auth_admin"} + try: + ku.get_remote_user(env) + except RuntimeError as err: + self.assertTrue(err.args[0].startswith("Malformed REMOTE_USER")) + else: + self.fail("Expected RuntimeError") + + def test_get_auth_data(self): + mc = FakeMemcache() + expiry = time() + 100 + ku.set_auth_data(mc, "root", "AUTH_tk", expiry, "root,admin") + (token, expires, groups) = ku.get_auth_data(mc, "root") + self.assertEqual(("AUTH_tk", expiry, "root,admin"), + (token, expires, groups)) + + def test_get_auth_data_err(self): + mc = FakeMemcache() + (token, expires, groups) = ku.get_auth_data(mc, "root") + self.assertEqual((token, expires, groups), (None, None, None)) + + expiry = time() - 1 + ku.set_auth_data(mc, "root", "AUTH_tk", expiry, "root,admin") + (token, expires, groups) = ku.get_auth_data(mc, "root") + self.assertEqual((token, expires, groups), (None, None, None)) + + def test_set_auth_data(self): + mc = FakeMemcache() + expiry = time() + 100 + ku.set_auth_data(mc, "root", "AUTH_tk", expiry, "root,admin") + + def test_generate_token(self): + token = ku.generate_token() + matches = re.match('AUTH_tk[a-f0-9]{32}', token) + self.assertIsNotNone(matches) + + def test_get_groups(self): + groups = ku.get_groups("root") + self.assertIn("root", groups) + + def test_get_groups_err(self): + try: + ku.get_groups("Zroot") + except RuntimeError as err: + self.assertTrue(err.args[0].startswith("Failure running id -G")) + else: + self.fail("Expected RuntimeError") -- cgit