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
|
:returns string: The base URL that will be used to talk to the
|
||||||
required service or None if not available.
|
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,
|
return service_catalog.url_for(service_type=service_type,
|
||||||
endpoint_type=interface,
|
endpoint_type=interface,
|
||||||
region_name=region_name)
|
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,
|
def request(self, url, method, json=None, original_ip=None,
|
||||||
user_agent=None, redirect=None, authenticated=None,
|
user_agent=None, redirect=None, authenticated=None,
|
||||||
endpoint_filter=None, auth=None, requests_auth=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.
|
"""Send an HTTP request with the specified characteristics.
|
||||||
|
|
||||||
Wrapper around `requests.Session.request` to handle tasks such as
|
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
|
:param bool raise_exc: If True then raise an appropriate exception for
|
||||||
failed HTTP requests. If False then return the
|
failed HTTP requests. If False then return the
|
||||||
request object. (optional, default True)
|
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
|
:param kwargs: any other parameter that can be passed to
|
||||||
requests.Session.request (such as `headers`). Except:
|
requests.Session.request (such as `headers`). Except:
|
||||||
'data' will be overwritten by the data in 'json' param.
|
'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)
|
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:
|
if raise_exc and resp.status_code >= 400:
|
||||||
_logger.debug('Request returned failure status: %s',
|
_logger.debug('Request returned failure status: %s',
|
||||||
resp.status_code)
|
resp.status_code)
|
||||||
@@ -408,3 +420,15 @@ class Session(object):
|
|||||||
'determine the endpoint URL.')
|
'determine the endpoint URL.')
|
||||||
|
|
||||||
return auth.get_endpoint(self, **kwargs)
|
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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
import httpretty
|
import httpretty
|
||||||
from six.moves import urllib
|
from six.moves import urllib
|
||||||
|
|
||||||
from keystoneclient.auth.identity import v2
|
from keystoneclient.auth.identity import v2
|
||||||
from keystoneclient import exceptions
|
from keystoneclient import exceptions
|
||||||
|
from keystoneclient.openstack.common import jsonutils
|
||||||
from keystoneclient import session
|
from keystoneclient import session
|
||||||
from keystoneclient.tests import utils
|
from keystoneclient.tests import utils
|
||||||
|
|
||||||
@@ -233,3 +236,26 @@ class V2IdentityPlugin(utils.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any',
|
self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any',
|
||||||
authenticated=True)
|
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 import access
|
||||||
from keystoneclient.auth.identity import v3
|
from keystoneclient.auth.identity import v3
|
||||||
from keystoneclient import exceptions
|
from keystoneclient import exceptions
|
||||||
|
from keystoneclient.openstack.common import jsonutils
|
||||||
from keystoneclient import session
|
from keystoneclient import session
|
||||||
from keystoneclient.tests import utils
|
from keystoneclient.tests import utils
|
||||||
|
|
||||||
@@ -388,3 +389,25 @@ class V3IdentityPlugin(utils.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any',
|
self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any',
|
||||||
authenticated=True)
|
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'}
|
'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.token = token
|
||||||
|
self._invalidate = invalidate
|
||||||
|
|
||||||
def get_token(self, session):
|
def get_token(self, session):
|
||||||
return self.token
|
return self.token
|
||||||
@@ -302,14 +303,19 @@ class AuthPlugin(base.BaseAuthPlugin):
|
|||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def invalidate(self):
|
||||||
|
return self._invalidate
|
||||||
|
|
||||||
|
|
||||||
class CalledAuthPlugin(base.BaseAuthPlugin):
|
class CalledAuthPlugin(base.BaseAuthPlugin):
|
||||||
|
|
||||||
ENDPOINT = 'http://fakeendpoint/'
|
ENDPOINT = 'http://fakeendpoint/'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, invalidate=True):
|
||||||
self.get_token_called = False
|
self.get_token_called = False
|
||||||
self.get_endpoint_called = False
|
self.get_endpoint_called = False
|
||||||
|
self.invalidate_called = False
|
||||||
|
self._invalidate = invalidate
|
||||||
|
|
||||||
def get_token(self, session):
|
def get_token(self, session):
|
||||||
self.get_token_called = True
|
self.get_token_called = True
|
||||||
@@ -319,6 +325,10 @@ class CalledAuthPlugin(base.BaseAuthPlugin):
|
|||||||
self.get_endpoint_called = True
|
self.get_endpoint_called = True
|
||||||
return self.ENDPOINT
|
return self.ENDPOINT
|
||||||
|
|
||||||
|
def invalidate(self):
|
||||||
|
self.invalidate_called = True
|
||||||
|
return self._invalidate
|
||||||
|
|
||||||
|
|
||||||
class SessionAuthTests(utils.TestCase):
|
class SessionAuthTests(utils.TestCase):
|
||||||
|
|
||||||
@@ -465,3 +475,34 @@ class SessionAuthTests(utils.TestCase):
|
|||||||
allow_redirects=mock.ANY,
|
allow_redirects=mock.ANY,
|
||||||
auth=requests_auth,
|
auth=requests_auth,
|
||||||
verify=mock.ANY)
|
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