devauth-middleware will not set its authorize func unless the token or account starts with the reseller prefix; if its not going to use its authorize func, it will set a deny-by-default func if one is not set already

This commit is contained in:
gholt 2010-09-17 19:32:27 +00:00 committed by Tarmac
commit 43b6a501f7
6 changed files with 214 additions and 49 deletions

View File

@ -60,9 +60,10 @@ Example Authentication with DevAuth:
* The external DevAuth server responds with "X-Auth-Groups:
test:tester,test,AUTH_storage_xyz"
* Now this user will have full access (via authorization procedures later)
to the AUTH_storage_xyz Swift storage account and access to other storage
accounts with the same `AUTH_` reseller prefix and has an ACL specifying
at least one of those three groups returned.
to the AUTH_storage_xyz Swift storage account and access to containers in
other storage accounts, provided the storage account begins with the same
`AUTH_` reseller prefix and the container has an ACL specifying at least
one of those three groups returned.
Authorization is performed through callbacks by the Swift Proxy server to the
WSGI environment's swift.authorize value, if one is set. The swift.authorize

View File

@ -454,7 +454,7 @@ YOU HAVE A FEW OPTIONS:
if create_reseller_admin and (
request.headers.get('X-Auth-Admin-User') != '.super_admin' or
request.headers.get('X-Auth-Admin-Key') != self.super_admin_key):
return HTTPForbidden(request=request)
return HTTPUnauthorized(request=request)
create_account_admin = \
request.headers.get('x-auth-user-admin') == 'true'
if create_account_admin and \
@ -484,7 +484,7 @@ YOU HAVE A FEW OPTIONS:
"""
if request.headers.get('X-Auth-Admin-User') != '.super_admin' or \
request.headers.get('X-Auth-Admin-Key') != self.super_admin_key:
return HTTPForbidden(request=request)
return HTTPUnauthorized(request=request)
result = self.recreate_accounts()
return Response(result, 200, request=request)

View File

@ -20,7 +20,7 @@ from webob.exc import HTTPForbidden, HTTPUnauthorized
from swift.common.bufferedhttp import http_connect_raw as http_connect
from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
from swift.common.utils import cache_from_env, split_path
from swift.common.utils import cache_from_env, split_path, TRUE_VALUES
class DevAuth(object):
@ -34,8 +34,7 @@ class DevAuth(object):
self.reseller_prefix += '_'
self.auth_host = conf.get('ip', '127.0.0.1')
self.auth_port = int(conf.get('port', 11000))
self.ssl = \
conf.get('ssl', 'false').lower() in ('true', 'on', '1', 'yes')
self.ssl = conf.get('ssl', 'false').lower() in TRUE_VALUES
self.timeout = int(conf.get('node_timeout', 10))
def __call__(self, env, start_response):
@ -44,38 +43,103 @@ class DevAuth(object):
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.
With a non-empty reseller prefix, acts as the definitive auth service
for just tokens and accounts that begin with that prefix, but will deny
requests outside this prefix if no other auth middleware overrides it.
With an empty reseller prefix, acts as the definitive auth service only
for tokens that validate to a non-empty set of groups. For all other
requests, acts as the fallback auth service when no other auth
middleware overrides it.
"""
groups = None
token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
if token and token.startswith(self.reseller_prefix):
memcache_client = cache_from_env(env)
key = '%s/token/%s' % (self.reseller_prefix, token)
cached_auth_data = memcache_client.get(key)
if cached_auth_data:
start, expiration, groups = cached_auth_data
if time() - start > expiration:
groups = None
if not groups:
with Timeout(self.timeout):
conn = http_connect(self.auth_host, self.auth_port, 'GET',
'/token/%s' % token, ssl=self.ssl)
resp = conn.getresponse()
resp.read()
conn.close()
if resp.status // 100 != 2:
# Note: Empty reseller_prefix will match all tokens.
# Attempt to auth my token with my auth server
groups = \
self.get_groups(token, memcache_client=cache_from_env(env))
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:
# Unauthorized token
if self.reseller_prefix:
# Because I know I'm the definitive auth for this token, I
# can deny it outright.
return HTTPUnauthorized()(env, start_response)
expiration = float(resp.getheader('x-auth-ttl'))
groups = resp.getheader('x-auth-groups')
# Because I'm not certain if I'm the definitive auth for empty
# reseller_prefixed tokens, I won't overwrite swift.authorize.
elif 'swift.authorize' not in env:
env['swift.authorize'] = self.denied_response
else:
if self.reseller_prefix:
# 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.
version, rest = split_path(env.get('PATH_INFO', ''),
1, 2, True)
if rest and rest.startswith(self.reseller_prefix):
# Handle anonymous access to accounts I'm the definitive
# auth for.
env['swift.authorize'] = self.authorize
env['swift.clean_acl'] = clean_acl
# Not my token, not my account, I can't authorize this request,
# deny all is a good idea if not already set...
elif 'swift.authorize' not in env:
env['swift.authorize'] = self.denied_response
# Because I'm not certain if I'm the definitive auth for empty
# reseller_prefixed accounts, I won't overwrite swift.authorize.
elif 'swift.authorize' not in env:
env['swift.authorize'] = self.authorize
env['swift.clean_acl'] = clean_acl
return self.app(env, start_response)
def get_groups(self, token, memcache_client=None):
"""
Get groups for the given token.
If memcache_client is set, token credentials will be cached
appropriately.
With a cache miss, or no memcache_client, the configurated external
authentication server will be queried for the group information.
:param token: Token to validate and return a group string for.
:param memcache_client: Memcached client to use for caching token
credentials; None if no caching is desired.
: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
key = '%s/token/%s' % (self.reseller_prefix, token)
cached_auth_data = memcache_client and memcache_client.get(key)
if cached_auth_data:
start, expiration, groups = cached_auth_data
if time() - start > expiration:
groups = None
if not groups:
with Timeout(self.timeout):
conn = http_connect(self.auth_host, self.auth_port, 'GET',
'/token/%s' % token, ssl=self.ssl)
resp = conn.getresponse()
resp.read()
conn.close()
if resp.status // 100 != 2:
return None
expiration = float(resp.getheader('x-auth-ttl'))
groups = resp.getheader('x-auth-groups')
if memcache_client:
memcache_client.set(key, (time(), expiration, groups),
timeout=expiration)
env['REMOTE_USER'] = groups
env['swift.authorize'] = self.authorize
env['swift.clean_acl'] = clean_acl
# We know the proxy logs the token, so we augment it just a bit to also
# log the authenticated user.
user = groups and groups.split(',', 1)[0] or ''
env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token)
return self.app(env, start_response)
return groups
def authorize(self, req):
"""
@ -117,6 +181,7 @@ 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 DevAuth(app, conf)
return auth_filter

View File

@ -56,7 +56,7 @@ _posix_fadvise = None
HASH_PATH_SUFFIX = os.environ.get('SWIFT_HASH_PATH_SUFFIX', 'endcap')
# Used when reading config values
TRUE_VALUES = set(('true', '1', 'yes', 'True', 'Yes'))
TRUE_VALUES = set(('true', '1', 'yes', 'True', 'Yes', 'on', 'On'))
def load_libc_function(func_name):

View File

@ -685,7 +685,8 @@ class TestAuthServer(unittest.TestCase):
conf = {'swift_dir': self.testdir, 'log_name': 'auth'}
self.assertRaises(ValueError, auth_server.AuthController, conf)
conf['super_admin_key'] = 'testkey'
auth_server.AuthController(conf)
controller = auth_server.AuthController(conf)
self.assertEquals(controller.super_admin_key, conf['super_admin_key'])
def test_add_storage_account(self):
auth_server.http_connect = fake_http_connect(201)

View File

@ -90,12 +90,23 @@ class Logger(object):
_, exc, _ = sys.exc_info()
self.exception_value = (msg,
'%s %s' % (exc.__class__.__name__, str(exc)), args, kwargs)
# tests
class FakeApp(object):
def __init__(self):
self.i_was_called = False
def __call__(self, env, start_response):
self.i_was_called = True
req = Request(env)
if 'swift.authorize' in env:
resp = env['swift.authorize'](req)
if resp:
return resp(env, start_response)
return ['204 No Content']
def start_response(*args):
pass
@ -104,6 +115,92 @@ class TestAuth(unittest.TestCase):
def setUp(self):
self.test_auth = auth.filter_factory({})(FakeApp())
def test_auth_deny_non_reseller_prefix(self):
old_http_connect = auth.http_connect
try:
auth.http_connect = mock_http_connect(204,
{'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'})
reqenv = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/BLAH_account',
'HTTP_X_AUTH_TOKEN': 'BLAH_t', 'swift.cache': FakeMemcache()}
result = ''.join(self.test_auth(reqenv, lambda x, y: None))
self.assert_(result.startswith('401'), result)
self.assertEquals(reqenv['swift.authorize'],
self.test_auth.denied_response)
finally:
auth.http_connect = old_http_connect
def test_auth_deny_non_reseller_prefix_no_override(self):
old_http_connect = auth.http_connect
try:
auth.http_connect = mock_http_connect(204,
{'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'})
fake_authorize = lambda x: lambda x, y: ['500 Fake']
reqenv = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/BLAH_account',
'HTTP_X_AUTH_TOKEN': 'BLAH_t', 'swift.cache': FakeMemcache(),
'swift.authorize': fake_authorize}
result = ''.join(self.test_auth(reqenv, lambda x, y: None))
self.assert_(result.startswith('500 Fake'), result)
self.assertEquals(reqenv['swift.authorize'], fake_authorize)
finally:
auth.http_connect = old_http_connect
def test_auth_no_reseller_prefix_deny(self):
# Ensures that when we have no reseller prefix, we don't deny a request
# outright but set up a denial swift.authorize and pass the request on
# down the chain.
old_http_connect = auth.http_connect
try:
local_app = FakeApp()
local_auth = \
auth.filter_factory({'reseller_prefix': ''})(local_app)
auth.http_connect = mock_http_connect(404)
reqenv = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/account',
'HTTP_X_AUTH_TOKEN': 't', 'swift.cache': FakeMemcache()}
result = ''.join(local_auth(reqenv, lambda x, y: None))
self.assert_(result.startswith('401'), result)
self.assert_(local_app.i_was_called)
self.assertEquals(reqenv['swift.authorize'],
local_auth.denied_response)
finally:
auth.http_connect = old_http_connect
def test_auth_no_reseller_prefix_allow(self):
# Ensures that when we have no reseller prefix, we can still allow
# access if our auth server accepts requests
old_http_connect = auth.http_connect
try:
local_app = FakeApp()
local_auth = \
auth.filter_factory({'reseller_prefix': ''})(local_app)
auth.http_connect = mock_http_connect(204,
{'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'})
reqenv = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/act',
'HTTP_X_AUTH_TOKEN': 't', 'swift.cache': None}
result = ''.join(local_auth(reqenv, lambda x, y: None))
self.assert_(result.startswith('204'), result)
self.assert_(local_app.i_was_called)
self.assertEquals(reqenv['swift.authorize'],
local_auth.authorize)
finally:
auth.http_connect = old_http_connect
def test_auth_no_reseller_prefix_no_token(self):
# Check that normally we set up a call back to our authorize.
local_auth = \
auth.filter_factory({'reseller_prefix': ''})(FakeApp())
reqenv = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/v1/account',
'swift.cache': FakeMemcache()}
result = ''.join(local_auth(reqenv, lambda x, y: None))
self.assert_(result.startswith('401'), result)
self.assertEquals(reqenv['swift.authorize'], local_auth.authorize)
# Now make sure we don't override an existing swift.authorize when we
# have no reseller prefix.
local_authorize = lambda req: None
reqenv['swift.authorize'] = local_authorize
result = ''.join(local_auth(reqenv, lambda x, y: None))
self.assert_(result.startswith('204'), result)
self.assertEquals(reqenv['swift.authorize'], local_authorize)
def test_auth_fail(self):
old_http_connect = auth.http_connect
try:
@ -121,8 +218,8 @@ class TestAuth(unittest.TestCase):
auth.http_connect = mock_http_connect(204,
{'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'})
result = ''.join(self.test_auth({'REQUEST_METHOD': 'GET',
'HTTP_X_AUTH_TOKEN': 'AUTH_t', 'swift.cache': FakeMemcache()},
lambda x, y: None))
'PATH_INFO': '/v/AUTH_cfa', 'HTTP_X_AUTH_TOKEN': 'AUTH_t',
'swift.cache': FakeMemcache()}, lambda x, y: None))
self.assert_(result.startswith('204'), result)
finally:
auth.http_connect = old_http_connect
@ -134,14 +231,14 @@ class TestAuth(unittest.TestCase):
auth.http_connect = mock_http_connect(204,
{'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'})
result = ''.join(self.test_auth({'REQUEST_METHOD': 'GET',
'HTTP_X_AUTH_TOKEN': 'AUTH_t', 'swift.cache': fake_memcache},
lambda x, y: None))
'PATH_INFO': '/v/AUTH_cfa', 'HTTP_X_AUTH_TOKEN': 'AUTH_t',
'swift.cache': fake_memcache}, lambda x, y: None))
self.assert_(result.startswith('204'), result)
auth.http_connect = mock_http_connect(404)
# Should still be in memcache
result = ''.join(self.test_auth({'REQUEST_METHOD': 'GET',
'HTTP_X_AUTH_TOKEN': 'AUTH_t', 'swift.cache': fake_memcache},
lambda x, y: None))
'PATH_INFO': '/v/AUTH_cfa', 'HTTP_X_AUTH_TOKEN': 'AUTH_t',
'swift.cache': fake_memcache}, lambda x, y: None))
self.assert_(result.startswith('204'), result)
finally:
auth.http_connect = old_http_connect
@ -153,8 +250,8 @@ class TestAuth(unittest.TestCase):
auth.http_connect = mock_http_connect(204,
{'x-auth-ttl': '0', 'x-auth-groups': 'act:usr,act,AUTH_cfa'})
result = ''.join(self.test_auth({'REQUEST_METHOD': 'GET',
'HTTP_X_AUTH_TOKEN': 'AUTH_t', 'swift.cache': fake_memcache},
lambda x, y: None))
'PATH_INFO': '/v/AUTH_cfa', 'HTTP_X_AUTH_TOKEN': 'AUTH_t',
'swift.cache': fake_memcache}, lambda x, y: None))
self.assert_(result.startswith('204'), result)
auth.http_connect = mock_http_connect(404)
# Should still be in memcache, but expired
@ -170,7 +267,8 @@ class TestAuth(unittest.TestCase):
try:
auth.http_connect = mock_http_connect(204,
{'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'})
req = Request.blank('/v/a/c/o', headers={'x-auth-token': 'AUTH_t'})
req = Request.blank('/v/AUTH_cfa/c/o',
headers={'x-auth-token': 'AUTH_t'})
req.environ['swift.cache'] = FakeMemcache()
result = ''.join(self.test_auth(req.environ, start_response))
self.assert_(result.startswith('204'), result)
@ -183,10 +281,10 @@ class TestAuth(unittest.TestCase):
try:
auth.http_connect = mock_http_connect(204,
{'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'})
req = Request.blank('/v/a/c/o')
req = Request.blank('/v/AUTH_cfa/c/o')
req.environ['swift.cache'] = FakeMemcache()
result = ''.join(self.test_auth(req.environ, start_response))
self.assert_(result.startswith('204'), result)
self.assert_(result.startswith('401'), result)
self.assert_(not req.remote_user, req.remote_user)
finally:
auth.http_connect = old_http_connect
@ -196,7 +294,7 @@ class TestAuth(unittest.TestCase):
try:
auth.http_connect = mock_http_connect(204,
{'x-auth-ttl': '1234', 'x-auth-groups': 'act:usr,act,AUTH_cfa'})
req = Request.blank('/v/a/c/o',
req = Request.blank('/v/AUTH_cfa/c/o',
headers={'x-storage-token': 'AUTH_t'})
req.environ['swift.cache'] = FakeMemcache()
result = ''.join(self.test_auth(req.environ, start_response))