From ca9d9d081c28718f2724ecec142517fcf8f04f3a Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Tue, 7 Jun 2016 16:36:40 -0700 Subject: [PATCH] Add delay_auth_decision config option This closely mirrors the same option in Keystone's authtoken middleware. Change-Id: I6395da659a5694a6e903862c7f1abefed00d50a2 --- etc/proxy-server.conf-sample | 5 ++ swift3/s3_token_middleware.py | 41 ++++++++--- swift3/test/unit/test_s3_token_middleware.py | 71 ++++++++++++++++++++ 3 files changed, 107 insertions(+), 10 deletions(-) diff --git a/etc/proxy-server.conf-sample b/etc/proxy-server.conf-sample index 7b2af55a..e2ca4d93 100644 --- a/etc/proxy-server.conf-sample +++ b/etc/proxy-server.conf-sample @@ -146,6 +146,11 @@ use = egg:swift3#s3token # Prefix that will be prepended to the tenant to form the account reseller_prefix = AUTH_ +# By default, s3token will reject all invalid S3-style requests. Set this to +# True to delegate that decision to downstream WSGI components. This may be +# useful if there are multiple auth systems in the proxy pipeline. +delay_auth_decision = False + # Keystone server details auth_uri = http://keystonehost:35357/ diff --git a/swift3/s3_token_middleware.py b/swift3/s3_token_middleware.py index 86773536..bc62f871 100644 --- a/swift3/s3_token_middleware.py +++ b/swift3/s3_token_middleware.py @@ -57,8 +57,10 @@ class S3Token(object): self._logger = logging.getLogger(conf.get('log_name', __name__)) self._logger.debug('Starting the %s component', PROTOCOL_NAME) self._reseller_prefix = conf.get('reseller_prefix', 'AUTH_') - # where to find the auth service (we use this to validate tokens) + self._delay_auth_decision = config_true_value( + conf.get('delay_auth_decision')) + # where to find the auth service (we use this to validate tokens) self._request_uri = conf.get('auth_uri') if not self._request_uri: self._logger.warning( @@ -153,9 +155,15 @@ class S3Token(object): try: access, signature = auth_header.split(' ')[-1].rsplit(':', 1) except ValueError: - msg = 'You have an invalid Authorization header: %s' - self._logger.debug(msg, auth_header) - return self._deny_request('InvalidURI')(environ, start_response) + if self._delay_auth_decision: + self._logger.debug('Invalid Authorization header: %s - ' + 'deferring reject downstream', auth_header) + return self._app(environ, start_response) + else: + self._logger.debug('Invalid Authorization header: %s - ' + 'rejecting request', auth_header) + return self._deny_request('InvalidURI')( + environ, start_response) # NOTE(chmou): This is to handle the special case with nova # when we have the option s3_affix_tenant. We will force it to @@ -191,9 +199,14 @@ class S3Token(object): resp = self._json_request(creds_json) except ServiceError as e: resp = e.args[0] # NB: swob.Response, not requests.Response - msg = 'Received error, exiting middleware with error: %s' - self._logger.debug(msg, resp.status_int) - return resp(environ, start_response) + if self._delay_auth_decision: + msg = 'Received error, deferring rejection based on error: %s' + self._logger.debug(msg, resp.status) + return self._app(environ, start_response) + else: + msg = 'Received error, rejecting request with error: %s' + self._logger.debug(msg, resp.status) + return resp(environ, start_response) self._logger.debug('Keystone Reply: Status: %d, Output: %s', resp.status_code, resp.content) @@ -203,9 +216,17 @@ class S3Token(object): token_id = str(identity_info['access']['token']['id']) tenant = identity_info['access']['token']['tenant'] except (ValueError, KeyError): - error = 'Error on keystone reply: %d %s' - self._logger.debug(error, resp.status_code, resp.content) - return self._deny_request('InvalidURI')(environ, start_response) + if self._delay_auth_decision: + error = ('Error on keystone reply: %d %s - ' + 'deferring rejection downstream') + self._logger.debug(error, resp.status_code, resp.content) + return self._app(environ, start_response) + else: + error = ('Error on keystone reply: %d %s - ' + 'rejecting request') + self._logger.debug(error, resp.status_code, resp.content) + return self._deny_request('InvalidURI')( + environ, start_response) req.headers['X-Auth-Token'] = token_id tenant_to_connect = force_tenant or tenant['id'] diff --git a/swift3/test/unit/test_s3_token_middleware.py b/swift3/test/unit/test_s3_token_middleware.py index 3fee001b..f1752b7a 100644 --- a/swift3/test/unit/test_s3_token_middleware.py +++ b/swift3/test/unit/test_s3_token_middleware.py @@ -62,8 +62,10 @@ class TestResponse(requests.Response): class FakeApp(object): + calls = 0 """This represents a WSGI app protected by the auth_token middleware.""" def __call__(self, env, start_response): + self.calls += 1 resp = Response() resp.environ = env return resp(env, start_response) @@ -148,6 +150,7 @@ class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase): req.get_response(self.middleware) self.assertTrue(req.path.startswith('/v1/AUTH_TENANT_ID')) self.assertEqual(req.headers['X-Auth-Token'], 'TOKEN_ID') + self.assertEqual(1, self.middleware._app.calls) def test_authorized_http(self): protocol = 'http' @@ -255,6 +258,7 @@ class S3TokenMiddlewareTestBad(S3TokenMiddlewareTestBase): self.assertEqual( resp.status_int, # pylint: disable-msg=E1101 s3_denied_req.status_int) # pylint: disable-msg=E1101 + self.assertEqual(0, self.middleware._app.calls) def test_bogus_authorization(self): req = Request.blank('/v1/AUTH_cfa/c/o') @@ -267,6 +271,7 @@ class S3TokenMiddlewareTestBad(S3TokenMiddlewareTestBase): self.assertEqual( resp.status_int, # pylint: disable-msg=E1101 s3_invalid_req.status_int) # pylint: disable-msg=E1101 + self.assertEqual(0, self.middleware._app.calls) def test_fail_to_connect_to_keystone(self): with mock.patch.object(self.middleware, '_json_request') as o: @@ -281,6 +286,7 @@ class S3TokenMiddlewareTestBad(S3TokenMiddlewareTestBase): self.assertEqual( resp.status_int, # pylint: disable-msg=E1101 s3_invalid_req.status_int) # pylint: disable-msg=E1101 + self.assertEqual(0, self.middleware._app.calls) def test_bad_reply(self): self.requests_mock.post(self.TEST_URL, @@ -296,3 +302,68 @@ class S3TokenMiddlewareTestBad(S3TokenMiddlewareTestBase): self.assertEqual( resp.status_int, # pylint: disable-msg=E1101 s3_invalid_req.status_int) # pylint: disable-msg=E1101 + self.assertEqual(0, self.middleware._app.calls) + + +class S3TokenMiddlewareTestDeferredAuth(S3TokenMiddlewareTestBase): + def setUp(self): + super(S3TokenMiddlewareTestDeferredAuth, self).setUp() + self.conf['delay_auth_decision'] = 'yes' + self.middleware = s3_token.S3Token(FakeApp(), self.conf) + + def test_unauthorized_token(self): + ret = {"error": + {"message": "EC2 access key not found.", + "code": 401, + "title": "Unauthorized"}} + self.requests_mock.post(self.TEST_URL, status_code=403, json=ret) + req = Request.blank('/v1/AUTH_cfa/c/o') + req.headers['Authorization'] = 'AWS access:signature' + req.headers['X-Storage-Token'] = 'token' + resp = req.get_response(self.middleware) + self.assertEqual( + resp.status_int, # pylint: disable-msg=E1101 + 200) + self.assertNotIn('X-Auth-Token', req.headers) + self.assertEqual(1, self.middleware._app.calls) + + def test_bogus_authorization(self): + req = Request.blank('/v1/AUTH_cfa/c/o') + req.headers['Authorization'] = 'AWS badboy' + req.headers['X-Storage-Token'] = 'token' + resp = req.get_response(self.middleware) + self.assertEqual( + resp.status_int, # pylint: disable-msg=E1101 + 200) + self.assertNotIn('X-Auth-Token', req.headers) + self.assertEqual(1, self.middleware._app.calls) + + def test_fail_to_connect_to_keystone(self): + with mock.patch.object(self.middleware, '_json_request') as o: + s3_invalid_req = self.middleware._deny_request('InvalidURI') + o.side_effect = s3_token.ServiceError(s3_invalid_req) + + req = Request.blank('/v1/AUTH_cfa/c/o') + req.headers['Authorization'] = 'AWS access:signature' + req.headers['X-Storage-Token'] = 'token' + resp = req.get_response(self.middleware) + self.assertEqual( + resp.status_int, # pylint: disable-msg=E1101 + 200) + self.assertNotIn('X-Auth-Token', req.headers) + self.assertEqual(1, self.middleware._app.calls) + + def test_bad_reply(self): + self.requests_mock.post(self.TEST_URL, + status_code=201, + text="") + + req = Request.blank('/v1/AUTH_cfa/c/o') + req.headers['Authorization'] = 'AWS access:signature' + req.headers['X-Storage-Token'] = 'token' + resp = req.get_response(self.middleware) + self.assertEqual( + resp.status_int, # pylint: disable-msg=E1101 + 200) + self.assertNotIn('X-Auth-Token', req.headers) + self.assertEqual(1, self.middleware._app.calls)