summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/swiftkerbauth_guide.md53
-rw-r--r--swiftkerbauth/kerbauth.py91
-rw-r--r--swiftkerbauth/kerbauth_utils.py13
-rw-r--r--test/unit/test_kerbauth.py87
4 files changed, 234 insertions, 10 deletions
diff --git a/doc/swiftkerbauth_guide.md b/doc/swiftkerbauth_guide.md
index ef76ad0..144bf59 100644
--- a/doc/swiftkerbauth_guide.md
+++ b/doc/swiftkerbauth_guide.md
@@ -4,6 +4,7 @@
* [Creating HTTP Service Principal on IPA server] (#http-principal)
* [Installing and configuring swiftkerbauth on IPA client] (#install-swiftkerbauth)
* [Using swiftkerbauth] (#use-swiftkerbauth)
+* [Configurable Parameters] (#config-swiftkerbauth)
<a name="httpd-kerb-install" />
## Installing Kerberos module for Apache on IPA client
@@ -433,3 +434,55 @@ The --negotiate option is for curl to perform Kerberos authentication and
--location-trusted is for curl to follow the redirect.
[auth_kerb_module Configuration]: http://modauthkerb.sourceforge.net/configure.html
+
+
+#### Get an authentication token when auth_mode=passive:
+> curl -v -H 'X-Auth-User: auth_admin' -H 'X-Auth-Key: Redhat*123' http://127.0.0.1:8080/auth/v1.0
+
+
+<a name="config-swiftkerbauth" />
+##Configurable Parameters
+
+The kerbauth filter section in **/etc/swift/proxy-server.conf** looks something
+like this:
+
+ [filter:kerbauth]
+ use = egg:swiftkerbauth#kerbauth
+ ext_authentication_url = http://client.rhelbox.com/cgi-bin/swift-auth
+ auth_method = active
+ token_life = 86400
+ debug_headers = yes
+ realm_name = RHELBOX.COM
+
+Of all the options listed above, specifying **ext\_authentication\_url** is
+mandatory. The rest of the options are optional and have default values.
+
+#### ext\_authentication\_url
+A URL specifying location of the swift-auth CGI script. Avoid using IP address.
+Default value: None
+
+#### token_life
+After how many seconds the cached information about an authentication token is
+discarded.
+Default value: 86400
+
+#### debug_headers
+When turned on, the response headers sent to the user will contain additional
+debug information apart from the auth token.
+Default value: yes
+
+#### auth_method
+Set this to **"active"** when you want to allow access **only to clients
+residing inside the domain**. In this mode, authentication is performed by
+mod\_auth\_kerb using the Kerberos ticket bundled with the client request.
+No username and password have to be specified to get a token.
+Set this to **"passive"** when you want to allow access to clients residing
+outside the domain. In this mode, authentication is performed by gleaning
+username and password from request headers (X-Auth-User and X-Auth-Key) and
+running kinit command against it.
+Default value: active
+
+#### realm_name
+This is applicable only when the auth_method=passive. This option specifies
+realm name if RHS server belongs to more than one realm and realm name is not
+part of the username specified in X-Auth-User header.
diff --git a/swiftkerbauth/kerbauth.py b/swiftkerbauth/kerbauth.py
index a1ba091..c51dc14 100644
--- a/swiftkerbauth/kerbauth.py
+++ b/swiftkerbauth/kerbauth.py
@@ -12,18 +12,24 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from time import time
+import errno
+from time import time, ctime
from traceback import format_exc
from eventlet import Timeout
-from swift.common.swob import Request
+from swift.common.swob import Request, Response
from swift.common.swob import HTTPBadRequest, HTTPForbidden, HTTPNotFound, \
- HTTPSeeOther
+ HTTPSeeOther, HTTPUnauthorized, HTTPServerError
from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
from swift.common.utils import cache_from_env, get_logger, \
split_path, config_true_value
+from swiftkerbauth.kerbauth_utils import get_auth_data, generate_token, \
+ set_auth_data, run_kinit
+from swiftkerbauth.kerbauth_utils import get_groups as \
+ get_groups_from_username
+
class KerbAuth(object):
"""
@@ -71,6 +77,10 @@ class KerbAuth(object):
if self.auth_prefix[-1] != '/':
self.auth_prefix += '/'
self.token_life = int(conf.get('token_life', 86400))
+ self.auth_method = conf.get('auth_method', 'active')
+ self.debug_headers = config_true_value(
+ conf.get('debug_headers', 'yes'))
+ self.realm_name = conf.get('realm_name', None)
self.allow_overrides = config_true_value(
conf.get('allow_overrides', 't'))
self.storage_url_scheme = conf.get('storage_url_scheme', 'default')
@@ -109,8 +119,13 @@ class KerbAuth(object):
env['reseller_request'] = True
else:
# Invalid token (may be expired)
- return HTTPSeeOther(
- location=self.ext_authentication_url)(env, start_response)
+ if self.auth_method == "active":
+ return HTTPSeeOther(
+ location=self.ext_authentication_url)(env,
+ start_response)
+ elif self.auth_method == "passive":
+ self.logger.increment('unauthorized')
+ return HTTPUnauthorized()(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
@@ -234,7 +249,11 @@ class KerbAuth(object):
self.logger.increment('forbidden')
return HTTPForbidden(request=req)
else:
- return HTTPSeeOther(location=self.ext_authentication_url)
+ if self.auth_method == "active":
+ return HTTPSeeOther(location=self.ext_authentication_url)
+ elif self.auth_method == "passive":
+ self.logger.increment('unauthorized')
+ return HTTPUnauthorized(request=req)
def handle(self, env, start_response):
"""
@@ -315,7 +334,65 @@ class KerbAuth(object):
or pathsegs[0] in ('auth', 'v1.0')):
return HTTPBadRequest(request=req)
- return HTTPSeeOther(location=self.ext_authentication_url)
+ # Client is inside the domain
+ if self.auth_method == "active":
+ return HTTPSeeOther(location=self.ext_authentication_url)
+
+ # Client is outside the domain
+ elif self.auth_method == "passive":
+ user = None
+ key = None
+ # Extract username and password from request
+ user = req.headers.get('x-storage-user')
+ if not user:
+ user = req.headers.get('x-auth-user')
+ key = req.headers.get('x-storage-pass')
+ if not key:
+ key = req.headers.get('x-auth-key')
+
+ if (not user) and (not key):
+ # If both are not given, client may be part of the domain
+ return HTTPSeeOther(location=self.ext_authentication_url)
+ elif None in (key, user):
+ # If either user OR key is given, but not both
+ return HTTPUnauthorized(request=req)
+
+ # Run kinit on the user
+ if self.realm_name and "@" not in user:
+ user = user + "@" + self.realm_name
+ try:
+ ret = run_kinit(user, key)
+ except OSError as e:
+ if e.errno == errno.ENOENT:
+ return HTTPServerError("kinit command not found\n")
+ if ret != 0:
+ self.logger.warning("Failed: kinit %s", user)
+ return HTTPUnauthorized(request=req)
+ self.logger.debug("kinit succeeded")
+
+ if "@" in user:
+ user = user.split("@")[0]
+ mc = cache_from_env(req.environ)
+ if not mc:
+ raise Exception('Memcache required')
+ token, expires, groups = get_auth_data(mc, user)
+ if not token:
+ token = generate_token()
+ expires = time() + self.token_life
+ groups = get_groups_from_username(user)
+ set_auth_data(mc, user, token, expires, groups)
+
+ headers = {'X-Auth-Token': token,
+ 'X-Storage-Token': token}
+
+ if self.debug_headers:
+ headers.update({'X-Debug-Remote-User': user,
+ 'X-Debug-Groups:': groups,
+ 'X-Debug-Token-Life': self.token_life,
+ 'X-Debug-Token-Expires': ctime(expires)})
+
+ resp = Response(request=req, headers=headers)
+ return resp
def filter_factory(global_conf, **local_conf):
diff --git a/swiftkerbauth/kerbauth_utils.py b/swiftkerbauth/kerbauth_utils.py
index 507580e..8490d83 100644
--- a/swiftkerbauth/kerbauth_utils.py
+++ b/swiftkerbauth/kerbauth_utils.py
@@ -16,7 +16,7 @@
import re
import random
import grp
-import subprocess
+from subprocess import Popen, PIPE
from time import time
from swiftkerbauth import TOKEN_LIFE, RESELLER_PREFIX
@@ -87,7 +87,7 @@ def get_groups(username):
# 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)
+ p = Popen(['id', '-G', username], stdout=PIPE)
if p.wait() != 0:
raise RuntimeError("Failure running id -G for %s" % username)
(p_stdout, p_stderr) = p.communicate()
@@ -104,3 +104,12 @@ def get_groups(username):
groups = [username] + groups
groups = ','.join(groups)
return groups
+
+
+def run_kinit(username, password):
+ """Runs kinit command as a child process and returns the status code."""
+ kinit = Popen(['kinit', username],
+ stdin=PIPE, stdout=PIPE, stderr=PIPE)
+ kinit.stdin.write('%s\n' % password)
+ kinit.wait()
+ return kinit.returncode
diff --git a/test/unit/test_kerbauth.py b/test/unit/test_kerbauth.py
index 1771314..471ff58 100644
--- a/test/unit/test_kerbauth.py
+++ b/test/unit/test_kerbauth.py
@@ -13,9 +13,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import os
+import errno
import unittest
from time import time
-
+from mock import patch, Mock
from swiftkerbauth import kerbauth as auth
from test.unit import FakeMemcache
from swift.common.swob import Request, Response
@@ -79,6 +81,8 @@ class TestAuth(unittest.TestCase):
def setUp(self):
self.test_auth = auth.filter_factory({})(FakeApp())
+ self.test_auth_passive = \
+ auth.filter_factory({'auth_method': 'passive'})(FakeApp())
def _make_request(self, path, **kwargs):
req = Request.blank(path, **kwargs)
@@ -130,6 +134,19 @@ class TestAuth(unittest.TestCase):
self.assertEquals(req.environ['swift.authorize'],
self.test_auth.denied_response)
+ def test_passive_top_level_deny(self):
+ req = self._make_request('/')
+ resp = req.get_response(self.test_auth_passive)
+ self.assertEquals(resp.status_int, 401)
+ self.assertEquals(req.environ['swift.authorize'],
+ self.test_auth_passive.denied_response)
+
+ def test_passive_deny_invalid_token(self):
+ req = self._make_request('/v1/AUTH_account',
+ headers={'X-Auth-Token': 'AUTH_t'})
+ resp = req.get_response(self.test_auth_passive)
+ self.assertEquals(resp.status_int, 401)
+
def test_override_asked_for_and_allowed(self):
self.test_auth = \
auth.filter_factory({'allow_overrides': 'true'})(FakeApp())
@@ -249,6 +266,74 @@ class TestAuth(unittest.TestCase):
resp = self.test_auth.handle_get_token(req)
self.assertEquals(resp.status_int, 404)
+ def test_passive_handle_get_token_no_user_or_key(self):
+ #No user and key
+ req = self._make_request('/auth/v1.0')
+ resp = self.test_auth_passive.handle_get_token(req)
+ self.assertEquals(resp.status_int, REDIRECT_STATUS)
+ #User given but no key
+ req = self._make_request('/auth/v1.0',
+ headers={'X-Auth-User': 'blah'})
+ resp = self.test_auth_passive.handle_get_token(req)
+ self.assertEquals(resp.status_int, 401)
+
+ def test_passive_handle_get_token_no_kinit(self):
+ req = self._make_request('/auth/v1.0',
+ headers={'X-Auth-User': 'user',
+ 'X-Auth-Key': 'password'})
+ _mock_run_kinit = Mock(side_effect=OSError(errno.ENOENT,
+ os.strerror(errno.ENOENT)))
+ with patch('swiftkerbauth.kerbauth.run_kinit', _mock_run_kinit):
+ resp = self.test_auth_passive.handle_get_token(req)
+ self.assertEquals(resp.status_int, 500)
+ self.assertIn("kinit command not found", resp.body)
+ _mock_run_kinit.assert_called_once_with('user', 'password')
+
+ def test_passive_handle_get_token_kinit_fail(self):
+ req = self._make_request('/auth/v1.0',
+ headers={'X-Auth-User': 'user',
+ 'X-Auth-Key': 'password'})
+ _mock_run_kinit = Mock(return_value=1)
+ with patch('swiftkerbauth.kerbauth.run_kinit', _mock_run_kinit):
+ resp = self.test_auth_passive.handle_get_token(req)
+ self.assertEquals(resp.status_int, 401)
+ _mock_run_kinit.assert_called_once_with('user', 'password')
+
+ def test_passive_handle_get_token_kinit_success_token_not_present(self):
+ req = self._make_request('/auth/v1.0',
+ headers={'X-Auth-User': 'user',
+ 'X-Auth-Key': 'password'})
+ _mock_run_kinit = Mock(return_value=0)
+ _mock_get_groups = Mock(return_value="user,admins")
+ with patch('swiftkerbauth.kerbauth.run_kinit', _mock_run_kinit):
+ with patch('swiftkerbauth.kerbauth.get_groups_from_username',
+ _mock_get_groups):
+ resp = self.test_auth_passive.handle_get_token(req)
+ _mock_run_kinit.assert_called_once_with('user', 'password')
+ _mock_run_kinit.assert_called_once_with('user', 'password')
+ _mock_get_groups.assert_called_once_with('user')
+ self.assertEquals(resp.status_int, 200)
+ self.assertIsNotNone(resp.headers['X-Auth-Token'])
+ self.assertIsNotNone(resp.headers['X-Storage-Token'])
+
+ def test_passive_handle_get_token_kinit_realm_and_memcache(self):
+ req = self._make_request('/auth/v1.0',
+ headers={'X-Auth-User': 'user',
+ 'X-Auth-Key': 'password'})
+ req.environ['swift.cache'] = None
+ _auth_passive = \
+ auth.filter_factory({'auth_method': 'passive',
+ 'realm_name': 'EXAMPLE.COM'})(FakeApp())
+ _mock_run_kinit = Mock(return_value=0)
+ with patch('swiftkerbauth.kerbauth.run_kinit', _mock_run_kinit):
+ try:
+ _auth_passive.handle_get_token(req)
+ except Exception as e:
+ self.assertTrue(e.args[0].startswith("Memcache required"))
+ else:
+ self.fail("Expected Exception - Memcache required")
+ _mock_run_kinit.assert_called_once_with('user@EXAMPLE.COM', 'password')
+
def test_handle(self):
req = self._make_request('/auth/v1.0')
resp = req.get_response(self.test_auth)