From 6ec1782dcc13b77eba14d7ff1ace6c9bca997dc5 Mon Sep 17 00:00:00 2001 From: Maru Newby Date: Tue, 20 Mar 2012 22:19:36 -0700 Subject: Add support to swift_auth for tokenless authz * Updates keystone.middleware.swift_auth to allow token-less (unauthenticated) access for container sync (bug 954030) and permitted referrers (bug 924578). Change-Id: Ieccf458c44dfe55f546dc15c79704800dad59ac0 --- doc/source/configuringservices.rst | 3 + keystone/middleware/swift_auth.py | 106 ++++++++++++++++++++++++++---------- tests/test_swift_auth_middleware.py | 56 +++++++++---------- 3 files changed, 104 insertions(+), 61 deletions(-) diff --git a/doc/source/configuringservices.rst b/doc/source/configuringservices.rst index 3782f360..2b0a5d2c 100644 --- a/doc/source/configuringservices.rst +++ b/doc/source/configuringservices.rst @@ -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 diff --git a/keystone/middleware/swift_auth.py b/keystone/middleware/swift_auth.py index df62a834..b5f0a9b1 100644 --- a/keystone/middleware/swift_auth.py +++ b/keystone/middleware/swift_auth.py @@ -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:: @@ -95,15 +98,17 @@ class SwiftAuth(object): 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): @@ -119,9 +124,12 @@ class SwiftAuth(object): '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 @@ -169,26 +177,13 @@ class SwiftAuth(object): 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) @@ -206,6 +201,57 @@ class SwiftAuth(object): 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. diff --git a/tests/test_swift_auth_middleware.py b/tests/test_swift_auth_middleware.py index 04f806bb..2a79d0ee 100644 --- a/tests/test_swift_auth_middleware.py +++ b/tests/test_swift_auth_middleware.py @@ -20,21 +20,15 @@ from keystone.middleware import swift_auth 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: @@ -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', @@ -59,35 +55,34 @@ class SwiftAuth(unittest.TestCase): 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': ''} @@ -106,8 +101,7 @@ class TestAuthorize(unittest.TestCase): 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): -- cgit