Skip to content

Commit

Permalink
Add support to swift_auth for tokenless authz
Browse files Browse the repository at this point in the history
 * Updates keystone.middleware.swift_auth to allow token-less
   (unauthenticated) access for container sync (bug 954030) and
   permitted referrers (bug 924578).

Change-Id: Ieccf458c44dfe55f546dc15c79704800dad59ac0
  • Loading branch information
Maru Newby committed Mar 31, 2012
1 parent f9c7871 commit 6ec1782
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 61 deletions.
3 changes: 3 additions & 0 deletions doc/source/configuringservices.rst
Expand Up @@ -201,6 +201,9 @@ rather than it's built in 'tempauth'.

[filter:authtoken]
paste.filter_factory = keystone.middleware.auth_token:filter_factory
# Delaying the auth decision is required to support token-less
# usage for anonymous referrers ('.r:*').
delay_auth_decision = true
service_port = 5000
service_host = 127.0.0.1
auth_port = 35357
Expand Down
106 changes: 76 additions & 30 deletions keystone/middleware/swift_auth.py
Expand Up @@ -44,9 +44,12 @@ class SwiftAuth(object):
pipeline = catch_errors cache authtoken swiftauth proxy-server
Make sure you have the authtoken middleware before the swiftauth
middleware. The authtoken will take care of validating the user
and swiftauth middleware will authorize it. See the documentation
about how to configure the authtoken middleware.
middleware. authtoken will take care of validating the user and
swiftauth will authorize access. If support is required for
unvalidated users (as with anonymous access), authtoken will need
to be configured with delay_auth_decision set to true. See the
documentation for more detail on how to configure the authtoken
middleware.
Set account auto creation to true::
Expand Down Expand Up @@ -95,15 +98,17 @@ def __init__(self, app, conf):
def __call__(self, environ, start_response):
identity = self._keystone_identity(environ)

if not identity:
environ['swift.authorize'] = self.denied_response
return self.app(environ, start_response)
if identity:
self.logger.debug('Using identity: %r' % (identity))
environ['keystone.identity'] = identity
environ['REMOTE_USER'] = identity.get('tenant')
environ['swift.authorize'] = self.authorize
else:
self.logger.debug('Authorizing as anonymous')
environ['swift.authorize'] = self.authorize_anonymous

self.logger.debug("Using identity: %r" % (identity))
environ['keystone.identity'] = identity
environ['REMOTE_USER'] = identity.get('tenant')
environ['swift.authorize'] = self.authorize
environ['swift.clean_acl'] = swift_acl.clean_acl

return self.app(environ, start_response)

def _keystone_identity(self, environ):
Expand All @@ -119,9 +124,12 @@ def _keystone_identity(self, environ):
'roles': roles}
return identity

def _get_account_for_tenant(self, tenant_id):
return '%s%s' % (self.reseller_prefix, tenant_id)

def _reseller_check(self, account, tenant_id):
"""Check reseller prefix."""
return account == '%s%s' % (self.reseller_prefix, tenant_id)
return account == self._get_account_for_tenant(tenant_id)

def authorize(self, req):
env = req.environ
Expand Down Expand Up @@ -169,26 +177,13 @@ def authorize(self, req):
req.environ['swift_owner'] = True
return

# Allow container sync.
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 swift_utils.get_remote_client(req)
in self.allowed_sync_hosts)):
log_msg = 'allowing proxy %s for container-sync' % req.remote_addr
self.logger.debug(log_msg)
return

# Check if referrer is allowed.
referrers, roles = swift_acl.parse_acl(getattr(req, 'acl', None))
if swift_acl.referrer_allowed(req.referer, referrers):
#TODO(chmou): convert .rlistings to Keystone type role.
if obj or '.rlistings' in roles:
log_msg = 'authorizing %s via referer ACL' % req.referrer
self.logger.debug(log_msg)
return

authorized = self._authorize_unconfirmed_identity(req, obj, referrers,
roles)
if authorized:
return
elif authorized is not None:
return self.denied_response(req)

# Allow ACL at individual user level (tenant:user format)
Expand All @@ -206,6 +201,57 @@ def authorize(self, req):

return self.denied_response(req)

def authorize_anonymous(self, req):
"""
Authorize an anonymous request.
:returns: None if authorization is granted, an error page otherwise.
"""
try:
part = swift_utils.split_path(req.path, 1, 4, True)
version, account, container, obj = part
except ValueError:
return webob.exc.HTTPNotFound(request=req)

is_authoritative_authz = (account and
account.startswith(self.reseller_prefix))
if not is_authoritative_authz:
return self.denied_response(req)

referrers, roles = swift_acl.parse_acl(getattr(req, 'acl', None))
authorized = self._authorize_unconfirmed_identity(req, obj, referrers,
roles)
if not authorized:
return self.denied_response(req)

def _authorize_unconfirmed_identity(self, req, obj, referrers, roles):
""""
Perform authorization for access that does not require a
confirmed identity.
:returns: A boolean if authorization is granted or denied. None if
a determination could not be made.
"""
# Allow container sync.
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 swift_utils.get_remote_client(req)
in self.allowed_sync_hosts)):
log_msg = 'allowing proxy %s for container-sync' % req.remote_addr
self.logger.debug(log_msg)
return True

# Check if referrer is allowed.
if swift_acl.referrer_allowed(req.referer, referrers):
if obj or '.rlistings' in roles:
log_msg = 'authorizing %s via referer ACL' % req.referrer
self.logger.debug(log_msg)
return True
return False

def denied_response(self, req):
"""Deny WSGI Response.
Expand Down
56 changes: 25 additions & 31 deletions tests/test_swift_auth_middleware.py
Expand Up @@ -20,21 +20,15 @@


class FakeApp(object):
def __init__(self, status_headers_body_iter=None, acl=None, sync_key=None):
def __init__(self, status_headers_body_iter=None):
self.calls = 0
self.status_headers_body_iter = status_headers_body_iter
if not self.status_headers_body_iter:
self.status_headers_body_iter = iter([('404 Not Found', {}, '')])
self.acl = acl
self.sync_key = sync_key

def __call__(self, env, start_response):
self.calls += 1
self.request = webob.Request.blank('', environ=env)
if self.acl:
self.request.acl = self.acl
if self.sync_key:
self.request.environ['swift_sync_key'] = self.sync_key
if 'swift.authorize' in env:
resp = env['swift.authorize'](self.request)
if resp:
Expand All @@ -48,7 +42,9 @@ class SwiftAuth(unittest.TestCase):
def setUp(self):
self.test_auth = swift_auth.filter_factory({})(FakeApp())

def _make_request(self, path, headers=None, **kwargs):
def _make_request(self, path=None, headers=None, **kwargs):
if not path:
path = '/v1/%s/c/o' % self.test_auth._get_account_for_tenant('foo')
return webob.Request.blank(path, headers=headers, **kwargs)

def _get_identity_headers(self, status='Confirmed', tenant_id='1',
Expand All @@ -59,35 +55,34 @@ def _get_identity_headers(self, status='Confirmed', tenant_id='1',
X_ROLE=role,
X_USER=user)

def _get_successful_middleware(self):
response_iter = iter([('200 OK', {}, '')])
return swift_auth.filter_factory({})(FakeApp(response_iter))

def test_confirmed_identity_is_authorized(self):
role = self.test_auth.reseller_admin_role
headers = self._get_identity_headers(role=role)
req = self._make_request('/v1/AUTH_acct/c', headers)
response_iter = iter([('200 OK', {}, '')])
test_auth = swift_auth.filter_factory({})(
FakeApp(response_iter))
resp = req.get_response(test_auth)
self.assertEquals(resp.status_int, 200)

def test_invalid_identity_is_not_authorized(self):
headers = self._get_identity_headers(status='Invalid')
req = self._make_request('/v1/AUTH_acct', headers)
resp = req.get_response(self.test_auth)
self.assertEquals(resp.status_int, 401)
resp = req.get_response(self._get_successful_middleware())
self.assertEqual(resp.status_int, 200)

def test_auth_deny_token_not_for_account(self):
headers = self._get_identity_headers(role='AUTH_acct')
req = self._make_request('/v1/AUTH_1', headers)
def test_confirmed_identity_is_not_authorized(self):
headers = self._get_identity_headers()
req = self._make_request('/v1/AUTH_acct/c', headers)
resp = req.get_response(self.test_auth)
self.assertEquals(resp.status_int, 403)
self.assertEqual(resp.status_int, 403)

#NOTE(chmou): This should fail when we are going to add anonymous
#access back.
def test_default_forbidden(self):
headers = self._get_identity_headers()
req = self._make_request('/v1/AUTH_acct', headers)
def test_anonymous_is_authorized_for_permitted_referrer(self):
req = self._make_request(headers={'X_IDENTITY_STATUS': 'Invalid'})
req.acl = '.r:*'
resp = req.get_response(self._get_successful_middleware())
self.assertEqual(resp.status_int, 200)

def test_anonymous_is_not_authorized_for_unknown_reseller_prefix(self):
req = self._make_request(path='/v1/BLAH_foo/c/o',
headers={'X_IDENTITY_STATUS': 'Invalid'})
resp = req.get_response(self.test_auth)
self.assertEquals(resp.status_int, 403)
self.assertEqual(resp.status_int, 401)

def test_blank_reseller_prefix(self):
conf = {'reseller_prefix': ''}
Expand All @@ -106,8 +101,7 @@ def _make_request(self, path, **kwargs):
def _get_account(self, identity=None):
if not identity:
identity = self._get_identity()
return '%s%s' % (self.test_auth.reseller_prefix,
identity['tenant'][0])
return self.test_auth._get_account_for_tenant(identity['tenant'][0])

def _get_identity(self, tenant_id='tenant_id',
tenant_name='tenant_name', user='user', roles=None):
Expand Down

0 comments on commit 6ec1782

Please sign in to comment.