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:
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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()
|
||||
|
@@ -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())
|
||||
|
@@ -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())
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user