diff options
author | Joe Heck <heckj@mac.com> | 2012-11-01 15:36:31 -0700 |
---|---|---|
committer | Vishvananda Ishaya <vishvananda@gmail.com> | 2012-11-07 20:09:22 -0800 |
commit | 7cc02c80cfb1976271fa8b6271091fcd35c1cb34 (patch) | |
tree | eeaf2187cfd57f61b26f684fc1e52eb6c1e5cb43 | |
parent | 8ee69635066129b0029d61c4b8248420f994290e (diff) | |
download | keystone-7cc02c80cfb1976271fa8b6271091fcd35c1cb34.tar.gz keystone-7cc02c80cfb1976271fa8b6271091fcd35c1cb34.tar.xz keystone-7cc02c80cfb1976271fa8b6271091fcd35c1cb34.zip |
fixes bug 1074172
updated diablo token based on output from diablo/stable keystone
added expiry to example tokens for test_auth_middleware
added a stack based HTTP response to test_auth_middleware to verify
sequencing
Change-Id: I738b0e9c1a0e62ad86adb95ec0b73f621513f7d4
-rw-r--r-- | keystone/middleware/auth_token.py | 38 | ||||
-rw-r--r-- | tests/test_auth_token_middleware.py | 85 |
2 files changed, 120 insertions, 3 deletions
diff --git a/keystone/middleware/auth_token.py b/keystone/middleware/auth_token.py index 5c198e83..43261f29 100644 --- a/keystone/middleware/auth_token.py +++ b/keystone/middleware/auth_token.py @@ -159,6 +159,16 @@ opts = [ CONF.register_opts(opts, group='keystone_authtoken') +def will_expire_soon(expiry): + """ Determines if expiration is about to occur. + + :param expiry: a datetime of the expected expiration + :returns: boolean : true if expiration is within 30 seconds + """ + soon = (timeutils.utcnow() + datetime.timedelta(seconds=30)) + return expiry < soon + + class InvalidUserToken(Exception): pass @@ -230,6 +240,7 @@ class AuthProtocol(object): # Credentials used to verify this component with the Auth service since # validating tokens is a privileged call self.admin_token = self._conf_get('admin_token') + self.admin_token_expiry = None self.admin_user = self._conf_get('admin_user') self.admin_password = self._conf_get('admin_password') self.admin_tenant_name = self._conf_get('admin_tenant_name') @@ -345,12 +356,21 @@ class AuthProtocol(object): def get_admin_token(self): """Return admin token, possibly fetching a new one. + if self.admin_token_expiry is set from fetching an admin token, check + it for expiration, and request a new token is the existing token + is about to expire. + :return admin token id :raise ServiceError when unable to retrieve token from keystone """ + if self.admin_token_expiry: + if will_expire_soon(self.admin_token_expiry): + self.admin_token = None + if not self.admin_token: - self.admin_token = self._request_admin_token() + (self.admin_token, + self.admin_token_expiry) = self._request_admin_token() return self.admin_token @@ -455,11 +475,17 @@ class AuthProtocol(object): try: token = data['access']['token']['id'] + expiry = data['access']['token']['expires'] assert token - return token + assert expiry + datetime_expiry = timeutils.parse_isotime(expiry) + return (token, timeutils.normalize_time(datetime_expiry)) except (AssertionError, KeyError): LOG.warn("Unexpected response from keystone service: %s", data) raise ServiceError('invalid json response') + except (ValueError): + LOG.warn("Unable to parse expiration time from token: %s", data) + raise ServiceError('invalid json response') def _validate_user_token(self, user_token, retry=True): """Authenticate user using PKI @@ -771,10 +797,16 @@ class AuthProtocol(object): with open(self.revoked_file_name, 'w') as f: f.write(value) - def fetch_revocation_list(self): + def fetch_revocation_list(self, retry=True): headers = {'X-Auth-Token': self.get_admin_token()} response, data = self._json_request('GET', '/v2.0/tokens/revoked', additional_headers=headers) + if response.status == 401: + if retry: + LOG.info('Keystone rejected admin token %s, resetting admin ' + 'token', headers) + self.admin_token = None + return self.fetch_revocation_list(retry=False) if response.status != 200: raise ServiceError('Unable to fetch token revocation list.') if (not 'signed' in data): diff --git a/tests/test_auth_token_middleware.py b/tests/test_auth_token_middleware.py index 01b4070f..f3a38b05 100644 --- a/tests/test_auth_token_middleware.py +++ b/tests/test_auth_token_middleware.py @@ -69,6 +69,7 @@ TOKEN_RESPONSES = { 'access': { 'token': { 'id': UUID_TOKEN_DEFAULT, + 'expires': '2999-01-01T00:00:10Z', 'tenant': { 'id': 'tenant_id1', 'name': 'tenant_name1', @@ -89,6 +90,7 @@ TOKEN_RESPONSES = { 'access': { 'token': { 'id': VALID_DIABLO_TOKEN, + 'expires': '2999-01-01T00:00:10', 'tenantId': 'tenant_id1', }, 'user': { @@ -105,6 +107,7 @@ TOKEN_RESPONSES = { 'access': { 'token': { 'id': UUID_TOKEN_UNSCOPED, + 'expires': '2999-01-01T00:00:10Z', }, 'user': { 'id': 'user_id1', @@ -120,6 +123,7 @@ TOKEN_RESPONSES = { 'access': { 'token': { 'id': 'valid-token', + 'expires': '2999-01-01T00:00:10Z', 'tenant': { 'id': 'tenant_id1', 'name': 'tenant_name1', @@ -137,6 +141,8 @@ TOKEN_RESPONSES = { }, } +FAKE_RESPONSE_STACK = [] + # The data for these tests are signed using openssl and are stored in files # in the signing subdirectory. In order to keep the values consistent between @@ -223,6 +229,23 @@ class FakeHTTPResponse(object): return self.body +class FakeStackHTTPConnection(object): + + def __init__(self, *args, **kwargs): + pass + + def getresponse(self): + if len(FAKE_RESPONSE_STACK): + return FAKE_RESPONSE_STACK.pop() + return FakeHTTPResponse(500, jsonutils.dumps('UNEXPECTED RESPONSE')) + + def request(self, *_args, **_kwargs): + pass + + def close(self): + pass + + class FakeHTTPConnection(object): last_requested_url = '' @@ -340,6 +363,60 @@ class BaseAuthTokenMiddlewareTest(test.TestCase): self.response_headers = dict(headers) +class StackResponseAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest): + """Auth Token middleware test setup that allows the tests to define + a stack of responses to HTTP requests in the test and get those + responses back in sequence for testing. + + Example:: + + resp1 = FakeHTTPResponse(401, jsonutils.dumps('')) + resp2 = FakeHTTPResponse(200, jsonutils.dumps({ + 'access': { + 'token': {'id': 'admin_token2'}, + }, + }) + FAKE_RESPONSE_STACK.append(resp1) + FAKE_RESPONSE_STACK.append(resp2) + + ... do your testing code here ... + + """ + + def setUp(self, expected_env=None): + super(StackResponseAuthTokenMiddlewareTest, self).setUp(expected_env) + self.middleware.http_client_class = FakeStackHTTPConnection + + def test_fetch_revocation_list_with_expire(self): + # first response to revocation list should return 401 Unauthorized + # to pretend to be an expired token + resp1 = FakeHTTPResponse(200, jsonutils.dumps({ + 'access': { + 'token': {'id': 'admin_token2'}, + }, + })) + resp2 = FakeHTTPResponse(401, jsonutils.dumps('')) + resp3 = FakeHTTPResponse(200, jsonutils.dumps({ + 'access': { + 'token': {'id': 'admin_token2'}, + }, + })) + resp4 = FakeHTTPResponse(200, SIGNED_REVOCATION_LIST) + + # first get_admin_token() call + FAKE_RESPONSE_STACK.append(resp1) + # request revocation list, get "unauthorized" due to simulated expired + # token + FAKE_RESPONSE_STACK.append(resp2) + # request a new admin_token + FAKE_RESPONSE_STACK.append(resp3) + # request revocation list, get the revocation list properly + FAKE_RESPONSE_STACK.append(resp4) + + fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list()) + self.assertEqual(fetched_list, REVOCATION_LIST) + + class DiabloAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest): """Auth Token middleware should understand Diablo keystone responses.""" def setUp(self): @@ -565,3 +642,11 @@ class AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest): self.assertEqual(self.response_status, 200) self.assertFalse(req.headers.get('X-Service-Catalog')) self.assertEqual(body, ['SUCCESS']) + + def test_will_expire_soon(self): + tenseconds = datetime.datetime.utcnow() + datetime.timedelta( + seconds=10) + self.assertTrue(auth_token.will_expire_soon(tenseconds)) + fortyseconds = datetime.datetime.utcnow() + datetime.timedelta( + seconds=40) + self.assertFalse(auth_token.will_expire_soon(fortyseconds)) |