Rebase swiftkerbauth imported code with upstream

Few changes have been merged to upstream swiftkerbauth repo.
This commit brings it down to gluster-swift repo.
Bringing below changes to gluster-swift repo in one go.

http://review.gluster.org/#/c/6296/
http://review.gluster.org/#/c/6370/
http://review.gluster.org/#/c/6595/
http://review.gluster.org/#/c/6713/
http://review.gluster.org/#/c/6732/

Change-Id: I10dc12d75ec63fca313339fbc71e4f18071af552
Signed-off-by: Chetan Risbud <crisbud@redhat.com>
Reviewed-on: http://review.gluster.org/6764
Reviewed-by: Prashanth Pai <ppai@redhat.com>
This commit is contained in:
Chetan Risbud 2014-01-23 12:42:53 +05:30
parent 1f432663ba
commit 03128e172e
8 changed files with 407 additions and 25 deletions

View File

@ -98,6 +98,18 @@ On client:
<a name="users-groups" />
###Adding users and groups
The following convention is to be followed in creating group names:
<reseller-prefix>\_<volume-name>
<reseller-prefix>\_<account-name>
As of now, account=volume=group
For example:
AUTH\_test
Adding groups and users to the Windows domain is easy task.
- Start -> Administrative Tools -> Active Directory Users & Computers

View File

@ -107,6 +107,18 @@ Check if reverse resolution works :
<a name="users-groups" />
## Adding users and groups
The following convention is to be followed in creating group names:
<reseller-prefix>\_<volume-name>
<reseller-prefix>\_<account-name>
As of now, account=volume=group
For example:
AUTH\_test
Create *auth_reseller_admin* user group
> ipa group-add auth_reseller_admin --desc="Full access to all Swift accounts"

View File

@ -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
@ -47,7 +48,8 @@ Copy keytab file to client:
Add a HTTP Kerberos service principal:
> c:\>ktpass.exe -princ HTTP/fcclient.winad.com@WINAD.COM -mapuser
> auth_admin@WINAD.COM -pass Redhat*123 -out c:\HTTP.keytab
> auth_admin@WINAD.COM -pass Redhat*123 -out c:\HTTP.keytab -crypto DES-CBC-CRC
> -kvno 0
Use winscp to copy HTTP.ketab file to /etc/httpd/conf/http.keytab
@ -101,6 +103,7 @@ Edit */etc/swift/proxy-server.conf* and add a new filter section as follows:
[filter:kerbauth]
use = egg:swiftkerbauth#kerbauth
ext_authentication_url = http://client.rhelbox.com/cgi-bin/swift-auth
auth_mode=passive
Add kerbauth to pipeline
@ -433,3 +436,56 @@ 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: test:auth_admin' -H 'X-Auth-Key: Redhat*123' http://127.0.0.1:8080/auth/v1.0
**NOTE**: X-Storage-Url response header can be returned only in passive mode.
<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: passive
#### 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.

View File

@ -24,7 +24,7 @@ from swift.common.memcached import MemcacheRing
from time import time, ctime
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
generate_token, set_auth_data, get_groups_from_username
def main():
@ -48,7 +48,7 @@ def main():
if not token:
token = generate_token()
expires = time() + TOKEN_LIFE
groups = get_groups(username)
groups = get_groups_from_username(username)
set_auth_data(mc, username, token, expires, groups)
print "X-Auth-Token: %s" % token

View File

@ -12,17 +12,22 @@
# 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 urllib import unquote
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 gluster.swift.common.middleware.swiftkerbauth.kerbauth_utils import \
get_auth_data, generate_token, \
set_auth_data, run_kinit, get_groups_from_username
class KerbAuth(object):
@ -71,6 +76,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', 'passive')
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 +118,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 +248,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):
"""
@ -290,16 +308,37 @@ class KerbAuth(object):
"""
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::
past.
"Active Mode" usage:
All formats require GSS (Kerberos) authentication.
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
and X-Storage-Token set to the token to use with Swift.
On successful authentication, the response will have X-Auth-Token
set to the token to use with Swift.
"Passive Mode" usage::
GET <auth-prefix>/v1/<act>/auth
X-Auth-User: <act>:<usr> or X-Storage-User: <usr>
X-Auth-Key: <key> or X-Storage-Pass: <key>
GET <auth-prefix>/auth
X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr>
X-Auth-Key: <key> or X-Storage-Pass: <key>
GET <auth-prefix>/v1.0
X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr>
X-Auth-Key: <key> or X-Storage-Pass: <key>
Values should be url encoded, "act%3Ausr" instead of "act:usr" for
example; however, for backwards compatibility the colon may be
included unencoded.
On successful authentication, the response will have X-Auth-Token
and X-Storage-Token set to the token to use with Swift and
X-Storage-URL set to the URL to the default Swift cluster to use.
:param req: The swob.Request to process.
:returns: swob.Response, 2xx on success with data set as explained
@ -315,7 +354,103 @@ 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":
account, user, key = None, None, None
# Extract user, account and key from request
if pathsegs[0] == 'v1' and pathsegs[2] == 'auth':
account = pathsegs[1]
user = req.headers.get('x-storage-user')
if not user:
user = unquote(req.headers.get('x-auth-user', ''))
if user:
if ':' not in user:
return HTTPUnauthorized(request=req)
else:
account2, user = user.split(':', 1)
if account != account2:
return HTTPUnauthorized(request=req)
key = req.headers.get('x-storage-pass')
if not key:
key = unquote(req.headers.get('x-auth-key', ''))
elif pathsegs[0] in ('auth', 'v1.0'):
user = unquote(req.headers.get('x-auth-user', ''))
if not user:
user = req.headers.get('x-storage-user')
if user:
if ':' not in user:
return HTTPUnauthorized(request=req)
else:
account, user = user.split(':', 1)
key = unquote(req.headers.get('x-auth-key', ''))
if not key:
key = req.headers.get('x-storage-pass')
if not (account or user or key):
# If all are not given, client may be part of the domain
return HTTPSeeOther(location=self.ext_authentication_url)
elif None in (key, user, account):
# If only one or two of them is given, but not all
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)
if ret == -1:
self.logger.warning("Failed: kinit: Password has probably "
"expired.")
return HTTPServerError("Kinit is taking too long.\n")
return HTTPUnauthorized(request=req)
self.logger.debug("kinit succeeded")
if "@" in user:
user = user.split("@")[0]
# Check if user really belongs to the account
groups_list = get_groups_from_username(user).strip().split(",")
user_group = ("%s%s" % (self.reseller_prefix, account)).lower()
reseller_admin_group = \
("%sreseller_admin" % self.reseller_prefix).lower()
if user_group not in groups_list:
# Check if user is reseller_admin. If not, return Unauthorized.
# On AD/IdM server, auth_reseller_admin is a separate group
if reseller_admin_group not in groups_list:
return HTTPUnauthorized(request=req)
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)
resp.headers['X-Storage-Url'] = \
'%s/v1/%s%s' % (resp.host_url, self.reseller_prefix, account)
return resp
def filter_factory(global_conf, **local_conf):

View File

@ -16,7 +16,8 @@
import re
import random
import grp
import subprocess
import signal
from subprocess import Popen, PIPE
from time import time
from gluster.swift.common.middleware.swiftkerbauth \
import TOKEN_LIFE, RESELLER_PREFIX
@ -82,13 +83,13 @@ def generate_token():
return token
def get_groups(username):
def get_groups_from_username(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)
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()
@ -105,3 +106,32 @@ 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)
# The following code handles a corner case where the Kerberos password
# has expired and a prompt is displayed to enter new password. Ideally,
# we would want to read from stdout but these are blocked reads. This is
# a hack to kill the process if it's taking too long!
class Alarm(Exception):
pass
def signal_handler(signum, frame):
raise Alarm
# Set the signal handler and a 1-second alarm
signal.signal(signal.SIGALRM, signal_handler)
signal.alarm(1)
try:
kinit.wait() # Wait for the child to exit
signal.alarm(0) # Reset the alarm
return kinit.returncode # Exit status of child on graceful exit
except Alarm:
# Taking too long, kill and return error
kinit.kill()
return -1

View File

@ -18,9 +18,9 @@ import errno
import unittest
from time import time
from mock import patch, Mock
from gluster.swift.common.middleware.swiftkerbauth import kerbauth as auth
from test.unit import FakeMemcache
from swift.common.swob import Request, Response
from gluster.swift.common.middleware.swiftkerbauth import kerbauth as auth
EXT_AUTHENTICATION_URL = "127.0.0.1"
REDIRECT_STATUS = 303 # HTTPSeeOther
@ -80,7 +80,8 @@ class TestKerbAuth(unittest.TestCase):
patch_filter_factory()
def setUp(self):
self.test_auth = auth.filter_factory({})(FakeApp())
self.test_auth = \
auth.filter_factory({'auth_method': 'active'})(FakeApp())
self.test_auth_passive = \
auth.filter_factory({'auth_method': 'passive'})(FakeApp())
@ -105,6 +106,10 @@ class TestKerbAuth(unittest.TestCase):
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()
@ -130,6 +135,19 @@ class TestKerbAuth(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 +267,126 @@ class TestKerbAuth(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': 'test:user'})
resp = self.test_auth_passive.handle_get_token(req)
self.assertEquals(resp.status_int, 401)
def test_passive_handle_get_token_account_in_req_path(self):
req = self._make_request('/v1/test/auth',
headers={'X-Auth-User': 'test:user',
'X-Auth-Key': 'password'})
_mock_run_kinit = Mock(return_value=0)
_mock_get_groups = Mock(return_value="user,auth_test")
with patch('gluster.swift.common.middleware.swiftkerbauth.kerbauth.run_kinit', _mock_run_kinit):
with patch('gluster.swift.common.middleware.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')
self.assertEquals(_mock_get_groups.call_count, 2)
self.assertEquals(resp.status_int, 200)
self.assertTrue(resp.headers['X-Auth-Token'] is not None)
self.assertTrue(resp.headers['X-Storage-Token'] is not None)
self.assertTrue(resp.headers['X-Storage-Url'] is not None)
def test_passive_handle_get_token_user_invalid_or_no__account(self):
#X-Auth-User not in acc:user format
req = self._make_request('/auth/v1.0',
headers={'X-Auth-User': 'user'})
resp = self.test_auth_passive.handle_get_token(req)
self.assertEquals(resp.status_int, 401)
req = self._make_request('/v1/test/auth',
headers={'X-Auth-User': 'user'})
resp = self.test_auth_passive.handle_get_token(req)
self.assertEquals(resp.status_int, 401)
# Account name mismatch
req = self._make_request('/v1/test/auth',
headers={'X-Auth-User': 'wrongacc:user'})
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': 'test:user',
'X-Auth-Key': 'password'})
_mock_run_kinit = Mock(side_effect=OSError(errno.ENOENT,
os.strerror(errno.ENOENT)))
with patch('gluster.swift.common.middleware.swiftkerbauth.kerbauth.run_kinit', _mock_run_kinit):
resp = self.test_auth_passive.handle_get_token(req)
self.assertEquals(resp.status_int, 500)
self.assertTrue("kinit command not found" in 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': 'test:user',
'X-Auth-Key': 'password'})
_mock_run_kinit = Mock(return_value=1)
with patch('gluster.swift.common.middleware.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': 'test:user',
'X-Auth-Key': 'password'})
_mock_run_kinit = Mock(return_value=0)
_mock_get_groups = Mock(return_value="user,auth_test")
with patch('gluster.swift.common.middleware.swiftkerbauth.kerbauth.run_kinit', _mock_run_kinit):
with patch('gluster.swift.common.middleware.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')
self.assertEquals(_mock_get_groups.call_count, 2)
self.assertEquals(resp.status_int, 200)
self.assertTrue(resp.headers['X-Auth-Token'] is not None)
self.assertTrue(resp.headers['X-Storage-Token'] is not None)
self.assertTrue(resp.headers['X-Storage-Url'] is not None)
def test_passive_handle_get_token_kinit_realm_and_memcache(self):
req = self._make_request('/auth/v1.0',
headers={'X-Auth-User': 'test: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)
_mock_get_groups = Mock(return_value="user,auth_test")
with patch('gluster.swift.common.middleware.swiftkerbauth.kerbauth.run_kinit', _mock_run_kinit):
with patch('gluster.swift.common.middleware.swiftkerbauth.kerbauth.get_groups_from_username',
_mock_get_groups):
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')
_mock_get_groups.assert_called_once_with('user')
def test_passive_handle_get_token_user_in_any__account(self):
req = self._make_request('/auth/v1.0',
headers={'X-Auth-User': 'test:user',
'X-Auth-Key': 'password'})
_mock_run_kinit = Mock(return_value=0)
_mock_get_groups = Mock(return_value="user,auth_blah")
with patch('gluster.swift.common.middleware.swiftkerbauth.kerbauth.run_kinit', _mock_run_kinit):
with patch('gluster.swift.common.middleware.swiftkerbauth.kerbauth.get_groups_from_username',
_mock_get_groups):
resp = self.test_auth_passive.handle_get_token(req)
self.assertEquals(resp.status_int, 401)
_mock_run_kinit.assert_called_once_with('user', 'password')
_mock_get_groups.assert_called_once_with('user')
def test_handle(self):
req = self._make_request('/auth/v1.0')
resp = req.get_response(self.test_auth)

View File

@ -17,7 +17,6 @@ import unittest
import re
from time import time
from test.unit import FakeMemcache
from gluster.swift.common.middleware.swiftkerbauth import kerbauth as auth
from gluster.swift.common.middleware.swiftkerbauth import kerbauth_utils as ku
@ -63,15 +62,15 @@ class TestKerbUtils(unittest.TestCase):
def test_generate_token(self):
token = ku.generate_token()
matches = re.match('AUTH_tk[a-f0-9]{32}', token)
self.assertNotEqual(matches, None)
self.assertTrue(matches is not None)
def test_get_groups(self):
groups = ku.get_groups("root")
def test_get_groups_from_username(self):
groups = ku.get_groups_from_username("root")
self.assertTrue("root" in groups)
def test_get_groups_err(self):
def test_get_groups_from_username_err(self):
try:
ku.get_groups("Zroot")
ku.get_groups_from_username("Zroot")
except RuntimeError as err:
self.assertTrue(err.args[0].startswith("Failure running id -G"))
else: