Auth Plugin invalidation

To allow session to re-fetch a token on an Unauthorized call we add an
invalidate method to auth plugins that is expected to flush all the
current authentication data from the plugin such that it will be
refreshed on next request.

This is then used to reissue requests from session when an Unauthorized
is called.

Change-Id: I98fa76fd67e97dc0a8c1ec0bf734792c337b5177
blueprint: keystoneclient-auth-token
This commit is contained in:
Jamie Lennox
2014-05-21 13:55:27 +10:00
parent 15bec1cde3
commit 5932fe1f83
6 changed files with 136 additions and 3 deletions

View File

@@ -53,3 +53,18 @@ class BaseAuthPlugin(object):
:returns string: The base URL that will be used to talk to the
required service or None if not available.
"""
def invalidate(self):
"""Invalidate the current authentication data.
This should result in fetching a new token on next call.
A plugin may be invalidated if an Unauthorized HTTP response is
returned to indicate that the token may have been revoked or is
otherwise now invalid.
:returns bool: True if there was something that the plugin did to
invalidate. This means that it makes sense to try again.
If nothing happens returns False to indicate give up.
"""
return False

View File

@@ -124,3 +124,7 @@ class BaseIdentityPlugin(base.BaseAuthPlugin):
return service_catalog.url_for(service_type=service_type,
endpoint_type=interface,
region_name=region_name)
def invalidate(self):
self.auth_ref = None
return True

View File

@@ -115,7 +115,7 @@ class Session(object):
def request(self, url, method, json=None, original_ip=None,
user_agent=None, redirect=None, authenticated=None,
endpoint_filter=None, auth=None, requests_auth=None,
raise_exc=True, **kwargs):
raise_exc=True, allow_reauth=True, **kwargs):
"""Send an HTTP request with the specified characteristics.
Wrapper around `requests.Session.request` to handle tasks such as
@@ -161,6 +161,9 @@ class Session(object):
:param bool raise_exc: If True then raise an appropriate exception for
failed HTTP requests. If False then return the
request object. (optional, default True)
:param bool allow_reauth: Allow fetching a new token and retrying the
request on receiving a 401 Unauthorized
response. (optional, default True)
:param kwargs: any other parameter that can be passed to
requests.Session.request (such as `headers`). Except:
'data' will be overwritten by the data in 'json' param.
@@ -256,6 +259,15 @@ class Session(object):
resp = self._send_request(url, method, redirect, **kwargs)
# handle getting a 401 Unauthorized response by invalidating the plugin
# and then retrying the request. This is only tried once.
if resp.status_code == 401 and authenticated and allow_reauth:
if self.invalidate(auth):
token = self.get_token(auth)
if token:
headers['X-Auth-Token'] = token
resp = self._send_request(url, method, redirect, **kwargs)
if raise_exc and resp.status_code >= 400:
_logger.debug('Request returned failure status: %s',
resp.status_code)
@@ -408,3 +420,15 @@ class Session(object):
'determine the endpoint URL.')
return auth.get_endpoint(self, **kwargs)
def invalidate(self, auth=None):
"""Invalidate an authentication plugin.
"""
if not auth:
auth = self.auth
if not auth:
msg = 'Auth plugin not available to invalidate'
raise exceptions.MissingAuthPlugin(msg)
return auth.invalidate()

View File

@@ -12,11 +12,14 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
import httpretty
from six.moves import urllib
from keystoneclient.auth.identity import v2
from keystoneclient import exceptions
from keystoneclient.openstack.common import jsonutils
from keystoneclient import session
from keystoneclient.tests import utils
@@ -233,3 +236,26 @@ class V2IdentityPlugin(utils.TestCase):
self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any',
authenticated=True)
@httpretty.activate
def test_invalidate_response(self):
resp_data1 = copy.deepcopy(self.TEST_RESPONSE_DICT)
resp_data2 = copy.deepcopy(self.TEST_RESPONSE_DICT)
resp_data1['access']['token']['id'] = 'token1'
resp_data2['access']['token']['id'] = 'token2'
auth_responses = [httpretty.Response(body=jsonutils.dumps(resp_data1),
status=200),
httpretty.Response(body=jsonutils.dumps(resp_data2),
status=200)]
self.stub_auth(responses=auth_responses)
a = v2.Password(self.TEST_URL, username=self.TEST_USER,
password=self.TEST_PASS)
s = session.Session(auth=a)
self.assertEqual('token1', s.get_token())
a.invalidate()
self.assertEqual('token2', s.get_token())

View File

@@ -20,6 +20,7 @@ from six.moves import urllib
from keystoneclient import access
from keystoneclient.auth.identity import v3
from keystoneclient import exceptions
from keystoneclient.openstack.common import jsonutils
from keystoneclient import session
from keystoneclient.tests import utils
@@ -388,3 +389,25 @@ class V3IdentityPlugin(utils.TestCase):
self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any',
authenticated=True)
@httpretty.activate
def test_invalidate_response(self):
body = jsonutils.dumps(self.TEST_RESPONSE_DICT)
auth_responses = [httpretty.Response(body=body,
X_Subject_Token='token1',
status=200),
httpretty.Response(body=body,
X_Subject_Token='token2',
status=200)]
httpretty.register_uri(httpretty.POST,
'%s/auth/tokens' % self.TEST_URL,
responses=auth_responses)
a = v3.Password(self.TEST_URL, username=self.TEST_USER,
password=self.TEST_PASS)
s = session.Session(auth=a)
self.assertEqual('token1', s.get_token())
a.invalidate()
self.assertEqual('token2', s.get_token())

View File

@@ -289,8 +289,9 @@ class AuthPlugin(base.BaseAuthPlugin):
'admin': 'http://image-admin:3333/v2.0'}
}
def __init__(self, token=TEST_TOKEN):
def __init__(self, token=TEST_TOKEN, invalidate=True):
self.token = token
self._invalidate = invalidate
def get_token(self, session):
return self.token
@@ -302,14 +303,19 @@ class AuthPlugin(base.BaseAuthPlugin):
except (KeyError, AttributeError):
return None
def invalidate(self):
return self._invalidate
class CalledAuthPlugin(base.BaseAuthPlugin):
ENDPOINT = 'http://fakeendpoint/'
def __init__(self):
def __init__(self, invalidate=True):
self.get_token_called = False
self.get_endpoint_called = False
self.invalidate_called = False
self._invalidate = invalidate
def get_token(self, session):
self.get_token_called = True
@@ -319,6 +325,10 @@ class CalledAuthPlugin(base.BaseAuthPlugin):
self.get_endpoint_called = True
return self.ENDPOINT
def invalidate(self):
self.invalidate_called = True
return self._invalidate
class SessionAuthTests(utils.TestCase):
@@ -465,3 +475,34 @@ class SessionAuthTests(utils.TestCase):
allow_redirects=mock.ANY,
auth=requests_auth,
verify=mock.ANY)
@httpretty.activate
def test_reauth_called(self):
auth = CalledAuthPlugin(invalidate=True)
sess = client_session.Session(auth=auth)
responses = [httpretty.Response(body='Failed', status=401),
httpretty.Response(body='Hello', status=200)]
httpretty.register_uri(httpretty.GET, self.TEST_URL,
responses=responses)
# allow_reauth=True is the default
resp = sess.get(self.TEST_URL, authenticated=True)
self.assertEqual(200, resp.status_code)
self.assertEqual('Hello', resp.text)
self.assertTrue(auth.invalidate_called)
@httpretty.activate
def test_reauth_not_called(self):
auth = CalledAuthPlugin(invalidate=True)
sess = client_session.Session(auth=auth)
responses = [httpretty.Response(body='Failed', status=401),
httpretty.Response(body='Hello', status=200)]
httpretty.register_uri(httpretty.GET, self.TEST_URL,
responses=responses)
self.assertRaises(exceptions.Unauthorized, sess.get, self.TEST_URL,
authenticated=True, allow_reauth=False)
self.assertFalse(auth.invalidate_called)