Merge "fixes bug 1074172"
This commit is contained in:
commit
9916227f97
@ -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):
|
||||
|
@ -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))
|
||||
|
Loading…
x
Reference in New Issue
Block a user