summaryrefslogtreecommitdiffstats
path: root/apachekerbauth
diff options
context:
space:
mode:
Diffstat (limited to 'apachekerbauth')
-rw-r--r--apachekerbauth/apachekerbauth.spec50
-rw-r--r--apachekerbauth/apachekerbauth/etc/httpd/conf.d/swift-auth.conf9
-rw-r--r--apachekerbauth/apachekerbauth/var/www/cgi-bin/memcached.py318
-rwxr-xr-xapachekerbauth/apachekerbauth/var/www/cgi-bin/swift-auth103
-rwxr-xr-xapachekerbauth/build.sh7
5 files changed, 487 insertions, 0 deletions
diff --git a/apachekerbauth/apachekerbauth.spec b/apachekerbauth/apachekerbauth.spec
new file mode 100644
index 0000000..cc6210a
--- /dev/null
+++ b/apachekerbauth/apachekerbauth.spec
@@ -0,0 +1,50 @@
+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 <clasohm@redhat.com> - 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
new file mode 100644
index 0000000..ba2b249
--- /dev/null
+++ b/apachekerbauth/apachekerbauth/etc/httpd/conf.d/swift-auth.conf
@@ -0,0 +1,9 @@
+<Location /cgi-bin/swift-auth>
+ AuthType Kerberos
+ AuthName "Swift Authentication"
+ KrbMethodNegotiate On
+ KrbMethodK5Passwd On
+ KrbAuthRealms EXAMPLE.COM
+ Krb5KeyTab /etc/httpd/conf/apache.keytab
+ require valid-user
+</Location>
diff --git a/apachekerbauth/apachekerbauth/var/www/cgi-bin/memcached.py b/apachekerbauth/apachekerbauth/var/www/cgi-bin/memcached.py
new file mode 100644
index 0000000..ecd9332
--- /dev/null
+++ b/apachekerbauth/apachekerbauth/var/www/cgi-bin/memcached.py
@@ -0,0 +1,318 @@
+# 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
new file mode 100755
index 0000000..d04ebb2
--- /dev/null
+++ b/apachekerbauth/apachekerbauth/var/www/cgi-bin/swift-auth
@@ -0,0 +1,103 @@
+#!/usr/bin/python
+
+# 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 "<pre>%i / %s</pre>" % 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
new file mode 100755
index 0000000..6db04ac
--- /dev/null
+++ b/apachekerbauth/build.sh
@@ -0,0 +1,7 @@
+#!/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