Merge "fixes bug 1074172"

This commit is contained in:
Jenkins 2012-11-08 18:33:05 +00:00 committed by Gerrit Code Review
commit 9916227f97
2 changed files with 120 additions and 3 deletions

View File

@ -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
@ -772,10 +798,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):

View File

@ -79,6 +79,7 @@ TOKEN_RESPONSES = {
'access': {
'token': {
'id': UUID_TOKEN_DEFAULT,
'expires': '2999-01-01T00:00:10Z',
'tenant': {
'id': 'tenant_id1',
'name': 'tenant_name1',
@ -99,6 +100,7 @@ TOKEN_RESPONSES = {
'access': {
'token': {
'id': VALID_DIABLO_TOKEN,
'expires': '2999-01-01T00:00:10',
'tenantId': 'tenant_id1',
},
'user': {
@ -115,6 +117,7 @@ TOKEN_RESPONSES = {
'access': {
'token': {
'id': UUID_TOKEN_UNSCOPED,
'expires': '2999-01-01T00:00:10Z',
},
'user': {
'id': 'user_id1',
@ -130,6 +133,7 @@ TOKEN_RESPONSES = {
'access': {
'token': {
'id': 'valid-token',
'expires': '2999-01-01T00:00:10Z',
'tenant': {
'id': 'tenant_id1',
'name': 'tenant_name1',
@ -147,6 +151,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
@ -237,6 +243,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 = ''
@ -354,6 +377,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):
@ -579,3 +656,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))