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:
commit
43b6a501f7
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
Loading…
Reference in New Issue
Block a user