summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README299
-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
-rwxr-xr-xswiftkerbauth/build.sh7
-rw-r--r--swiftkerbauth/swiftkerbauth.spec54
-rw-r--r--swiftkerbauth/swiftkerbauth/swiftkerbauth.py346
9 files changed, 1193 insertions, 0 deletions
diff --git a/README b/README
new file mode 100644
index 0000000..34b4ea7
--- /dev/null
+++ b/README
@@ -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
+...
+<pre>1365195860 / auth_admin,auth_reseller_admin</pre>
+
+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/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
diff --git a/swiftkerbauth/build.sh b/swiftkerbauth/build.sh
new file mode 100755
index 0000000..0daff96
--- /dev/null
+++ b/swiftkerbauth/build.sh
@@ -0,0 +1,7 @@
+#!/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/swiftkerbauth.spec b/swiftkerbauth/swiftkerbauth.spec
new file mode 100644
index 0000000..f01dc8d
--- /dev/null
+++ b/swiftkerbauth/swiftkerbauth.spec
@@ -0,0 +1,54 @@
+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 <<EOF
+
+[filter:kerbauth]
+paste.filter_factory = swiftkerbauth:filter_factory
+ext_authentication_url = http://AUTHENTICATION_SERVER/cgi-bin/swift-auth
+EOF
+fi
+
+%changelog
+* Fri Apr 5 2013 Carsten Clasohm <clasohm@redhat.com> - 1.0-1
+- initial build
diff --git a/swiftkerbauth/swiftkerbauth/swiftkerbauth.py b/swiftkerbauth/swiftkerbauth/swiftkerbauth.py
new file mode 100644
index 0000000..05ff6e7
--- /dev/null
+++ b/swiftkerbauth/swiftkerbauth/swiftkerbauth.py
@@ -0,0 +1,346 @@
+# Copyright (c) 2011 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.
+
+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 <auth-prefix>/v1/<act>/auth
+ GET <auth-prefix>/auth
+ GET <auth-prefix>/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