From da5932affc253a8b50ba753c6f9fabde68410501 Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Fri, 7 Sep 2018 11:31:50 -0700 Subject: [PATCH] Respect delay_auth_decision when Keystone is unavailable The delay_auth_decision option has two main uses: 1. Allow a service to provide its own auth mechanism, separate from auth tokens (like Swift's tempurl middleware). 2. Allow a service to integrate with multiple auth middlewares which may want to use the same X-Auth-Token header. The first case works fine even when the service has trouble talking to Keystone -- the client doesn't send an X-Auth-Token header, so we never even attempt to contact Keystone. The second case can be problematic, however. The client will provide some token, and we don't know whether it's valid for Keystone, the other auth system, or neither. We have to *try* contacting Keystone, but if that was down we'd previously return a 503 without ever trying the other auth system. As a result, a Keystone failure results in a total system failure. Now, when delay_auth_decision is True and we cannot determine whether a token is valid or invalid, we'll instead declare the token invalid and defer the rejection. As a result, Keystone failures only affect Keystone users, and tokens issued by the other auth system may still be validated and used. Change-Id: Ie4b3319862ba7fbd329dc6883ce837e894d5270c --- keystonemiddleware/auth_token/__init__.py | 5 ++ .../auth_token/test_auth_token_middleware.py | 66 +++++++++++++++++++ ..._auth_instead_of_503-f9b46bf4fbc11455.yaml | 9 +++ 3 files changed, 80 insertions(+) create mode 100644 releasenotes/notes/delay_auth_instead_of_503-f9b46bf4fbc11455.yaml diff --git a/keystonemiddleware/auth_token/__init__.py b/keystonemiddleware/auth_token/__init__.py index 4ac73886..003f14e6 100644 --- a/keystonemiddleware/auth_token/__init__.py +++ b/keystonemiddleware/auth_token/__init__.py @@ -768,6 +768,11 @@ class AuthProtocol(BaseAuthProtocol): ksm_exceptions.RevocationListError, ksm_exceptions.ServiceError) as e: self.log.critical('Unable to validate token: %s', e) + if self._delay_auth_decision: + self.log.debug('Keystone unavailable; marking token as ' + 'invalid and deferring auth decision.') + raise ksm_exceptions.InvalidToken( + 'Keystone unavailable: %s' % e) raise webob.exc.HTTPServiceUnavailable( 'The Keystone service is temporarily unavailable.') except ksm_exceptions.InvalidToken: diff --git a/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py b/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py index f9916ccd..9051d718 100644 --- a/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py +++ b/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py @@ -2043,6 +2043,18 @@ class v3AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, class DelayedAuthTests(BaseAuthTokenMiddlewareTest): + def token_response(self, request, context): + auth_id = request.headers.get('X-Auth-Token') + self.assertEqual(auth_id, FAKE_ADMIN_TOKEN_ID) + + if request.headers.get('X-Subject-Token') == ERROR_TOKEN: + msg = 'Network connection refused.' + raise ksc_exceptions.ConnectionRefused(msg) + + # All others just fail + context.status_code = 404 + return '' + def test_header_in_401(self): body = uuid.uuid4().hex www_authenticate_uri = 'http://local.test' @@ -2101,6 +2113,60 @@ class DelayedAuthTests(BaseAuthTokenMiddlewareTest): self.assertFalse(token_auth.has_service_token) self.assertIsNone(token_auth.service) + def test_auth_plugin_with_token(self): + self.requests_mock.get('%s/v3/auth/tokens' % BASE_URI, + text=self.token_response, + headers={'X-Subject-Token': uuid.uuid4().hex}) + + body = uuid.uuid4().hex + www_authenticate_uri = 'http://local.test' + conf = { + 'delay_auth_decision': 'True', + 'www_authenticate_uri': www_authenticate_uri, + 'auth_type': 'admin_token', + 'endpoint': '%s/v3' % BASE_URI, + 'token': FAKE_ADMIN_TOKEN_ID, + } + + middleware = self.create_simple_middleware(body=body, conf=conf) + resp = self.call(middleware, headers={ + 'X-Auth-Token': 'non-keystone-token'}) + self.assertEqual(six.b(body), resp.body) + + token_auth = resp.request.environ['keystone.token_auth'] + + self.assertFalse(token_auth.has_user_token) + self.assertIsNone(token_auth.user) + self.assertFalse(token_auth.has_service_token) + self.assertIsNone(token_auth.service) + + def test_auth_plugin_with_token_keystone_down(self): + self.requests_mock.get('%s/v3/auth/tokens' % BASE_URI, + text=self.token_response, + headers={'X-Subject-Token': ERROR_TOKEN}) + + body = uuid.uuid4().hex + www_authenticate_uri = 'http://local.test' + conf = { + 'delay_auth_decision': 'True', + 'www_authenticate_uri': www_authenticate_uri, + 'auth_type': 'admin_token', + 'endpoint': '%s/v3' % BASE_URI, + 'token': FAKE_ADMIN_TOKEN_ID, + 'http_request_max_retries': '0' + } + + middleware = self.create_simple_middleware(body=body, conf=conf) + resp = self.call(middleware, headers={'X-Auth-Token': ERROR_TOKEN}) + self.assertEqual(six.b(body), resp.body) + + token_auth = resp.request.environ['keystone.token_auth'] + + self.assertFalse(token_auth.has_user_token) + self.assertIsNone(token_auth.user) + self.assertFalse(token_auth.has_service_token) + self.assertIsNone(token_auth.service) + class CommonCompositeAuthTests(object): """Test Composite authentication. diff --git a/releasenotes/notes/delay_auth_instead_of_503-f9b46bf4fbc11455.yaml b/releasenotes/notes/delay_auth_instead_of_503-f9b46bf4fbc11455.yaml new file mode 100644 index 00000000..11ce28aa --- /dev/null +++ b/releasenotes/notes/delay_auth_instead_of_503-f9b46bf4fbc11455.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + When ``delay_auth_decision`` is enabled and a Keystone failure prevents + a final decision about whether a token is valid or invalid, it will be + marked invalid and the application will be responsible for a final auth + decision. This is similar to what happens when a token is confirmed *not* + valid. This allows a Keystone outage to only affect Keystone users in a + multi-auth system.