Merge "Auth Plugin invalidation"
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