Merge "Add get_headers interface to authentication plugins"
This commit is contained in:
commit
4ee6e3302a
keystoneclient
@ -21,6 +21,7 @@ __all__ = [
|
||||
'AUTH_INTERFACE',
|
||||
'BaseAuthPlugin',
|
||||
'get_plugin_class',
|
||||
'IDENTITY_AUTH_HEADER_NAME',
|
||||
'PLUGIN_NAMESPACE',
|
||||
|
||||
# auth.cli
|
||||
|
@ -10,7 +10,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import abc
|
||||
import os
|
||||
|
||||
import six
|
||||
@ -25,6 +24,7 @@ from keystoneclient import exceptions
|
||||
AUTH_INTERFACE = object()
|
||||
|
||||
PLUGIN_NAMESPACE = 'keystoneclient.auth.plugin'
|
||||
IDENTITY_AUTH_HEADER_NAME = 'X-Auth-Token'
|
||||
|
||||
|
||||
def get_plugin_class(name):
|
||||
@ -48,11 +48,9 @@ def get_plugin_class(name):
|
||||
return mgr.driver
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseAuthPlugin(object):
|
||||
"""The basic structure of an authentication plugin."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_token(self, session, **kwargs):
|
||||
"""Obtain a token.
|
||||
|
||||
@ -65,6 +63,15 @@ class BaseAuthPlugin(object):
|
||||
|
||||
Returning None will indicate that no token was able to be retrieved.
|
||||
|
||||
This function is misplaced as it should only be required for auth
|
||||
plugins that use the 'X-Auth-Token' header. However due to the way
|
||||
plugins evolved this method is required and often called to trigger an
|
||||
authentication request on a new plugin.
|
||||
|
||||
When implementing a new plugin it is advised that you implement this
|
||||
method, however if you don't require the 'X-Auth-Token' header override
|
||||
the `get_headers` method instead.
|
||||
|
||||
:param session: A session object so the plugin can make HTTP calls.
|
||||
:type session: keystoneclient.session.Session
|
||||
|
||||
@ -72,6 +79,44 @@ class BaseAuthPlugin(object):
|
||||
:rtype: string
|
||||
"""
|
||||
|
||||
def get_headers(self, session, **kwargs):
|
||||
"""Fetch authentication headers for message.
|
||||
|
||||
This is a more generalized replacement of the older get_token to allow
|
||||
plugins to specify different or additional authentication headers to
|
||||
the OpenStack standard 'X-Auth-Token' header.
|
||||
|
||||
How the authentication headers are obtained is up to the plugin. If the
|
||||
headers are still valid they may be re-used, retrieved from cache or
|
||||
the plugin may invoke an authentication request against a server.
|
||||
|
||||
The default implementation of get_headers calls the `get_token` method
|
||||
to enable older style plugins to continue functioning unchanged.
|
||||
Subclasses should feel free to completely override this function to
|
||||
provide the headers that they want.
|
||||
|
||||
There are no required kwargs. They are passed directly to the auth
|
||||
plugin and they are implementation specific.
|
||||
|
||||
Returning None will indicate that no token was able to be retrieved and
|
||||
that authorization was a failure. Adding no authentication data can be
|
||||
achieved by returning an empty dictionary.
|
||||
|
||||
:param session: The session object that the auth_plugin belongs to.
|
||||
:type session: keystoneclient.session.Session
|
||||
|
||||
:returns: Headers that are set to authenticate a message or None for
|
||||
failure. Note that when checking this value that the empty
|
||||
dict is a valid, non-failure response.
|
||||
:rtype: dict
|
||||
"""
|
||||
token = self.get_token(session)
|
||||
|
||||
if not token:
|
||||
return None
|
||||
|
||||
return {IDENTITY_AUTH_HEADER_NAME: token}
|
||||
|
||||
def get_endpoint(self, session, **kwargs):
|
||||
"""Return an endpoint for the client.
|
||||
|
||||
|
@ -299,12 +299,13 @@ class Session(object):
|
||||
authenticated = bool(auth or self.auth)
|
||||
|
||||
if authenticated:
|
||||
token = self.get_token(auth)
|
||||
auth_headers = self.get_auth_headers(auth)
|
||||
|
||||
if not token:
|
||||
raise exceptions.AuthorizationFailure(_("No token Available"))
|
||||
if auth_headers is None:
|
||||
msg = _('No valid authentication is available')
|
||||
raise exceptions.AuthorizationFailure(msg)
|
||||
|
||||
headers['X-Auth-Token'] = token
|
||||
headers.update(auth_headers)
|
||||
|
||||
if osprofiler_web:
|
||||
headers.update(osprofiler_web.get_trace_id_headers())
|
||||
@ -371,9 +372,10 @@ class Session(object):
|
||||
# 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
|
||||
auth_headers = self.get_auth_headers(auth)
|
||||
|
||||
if auth_headers is not None:
|
||||
headers.update(auth_headers)
|
||||
resp = send(**kwargs)
|
||||
|
||||
if raise_exc and resp.status_code >= 400:
|
||||
@ -563,6 +565,24 @@ class Session(object):
|
||||
|
||||
return auth
|
||||
|
||||
def get_auth_headers(self, auth=None, **kwargs):
|
||||
"""Return auth headers as provided by the auth plugin.
|
||||
|
||||
:param auth: The auth plugin to use for token. Overrides the plugin
|
||||
on the session. (optional)
|
||||
:type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin`
|
||||
|
||||
:raises keystoneclient.exceptions.AuthorizationFailure: if a new token
|
||||
fetch fails.
|
||||
:raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not
|
||||
available.
|
||||
|
||||
:returns: Authentication headers or None for failure.
|
||||
:rtype: dict
|
||||
"""
|
||||
auth = self._auth_required(auth, 'fetch a token')
|
||||
return auth.get_headers(self, **kwargs)
|
||||
|
||||
def get_token(self, auth=None):
|
||||
"""Return a token as provided by the auth plugin.
|
||||
|
||||
@ -575,16 +595,14 @@ class Session(object):
|
||||
:raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not
|
||||
available.
|
||||
|
||||
*DEPRECATED*: This assumes that the only header that is used to
|
||||
authenticate a message is 'X-Auth-Token'. This may not be
|
||||
correct. Use get_auth_headers instead.
|
||||
|
||||
:returns: A valid token.
|
||||
:rtype: string
|
||||
"""
|
||||
auth = self._auth_required(auth, 'fetch a token')
|
||||
|
||||
try:
|
||||
return auth.get_token(self)
|
||||
except exceptions.HttpError as exc:
|
||||
raise exceptions.AuthorizationFailure(
|
||||
_("Authentication failure: %s") % exc)
|
||||
return (self.get_auth_headers(auth) or {}).get('X-Auth-Token')
|
||||
|
||||
def get_endpoint(self, auth=None, **kwargs):
|
||||
"""Get an endpoint as provided by the auth plugin.
|
||||
|
@ -221,7 +221,7 @@ class CommonIdentityTests(object):
|
||||
s = session.Session(auth=a)
|
||||
|
||||
# trigger token fetching
|
||||
s.get_token()
|
||||
s.get_auth_headers()
|
||||
|
||||
self.assertTrue(a.auth_ref)
|
||||
self.assertTrue(a.invalidate())
|
||||
@ -368,3 +368,56 @@ class CatalogHackTests(utils.TestCase):
|
||||
version=(3, 0))
|
||||
|
||||
self.assertEqual(self.V2_URL, endpoint)
|
||||
|
||||
|
||||
class GenericPlugin(base.BaseAuthPlugin):
|
||||
|
||||
BAD_TOKEN = uuid.uuid4().hex
|
||||
|
||||
def __init__(self):
|
||||
super(GenericPlugin, self).__init__()
|
||||
|
||||
self.endpoint = 'http://keystone.host:5000'
|
||||
|
||||
self.headers = {'headerA': 'valueA',
|
||||
'headerB': 'valueB'}
|
||||
|
||||
def url(self, prefix):
|
||||
return '%s/%s' % (self.endpoint, prefix)
|
||||
|
||||
def get_token(self, session, **kwargs):
|
||||
# NOTE(jamielennox): by specifying get_headers this should not be used
|
||||
return self.BAD_TOKEN
|
||||
|
||||
def get_headers(self, session, **kwargs):
|
||||
return self.headers
|
||||
|
||||
def get_endpoint(self, session, **kwargs):
|
||||
return self.endpoint
|
||||
|
||||
|
||||
class GenericAuthPluginTests(utils.TestCase):
|
||||
|
||||
# filter doesn't matter to GenericPlugin, but we have to specify one
|
||||
ENDPOINT_FILTER = {uuid.uuid4().hex: uuid.uuid4().hex}
|
||||
|
||||
def setUp(self):
|
||||
super(GenericAuthPluginTests, self).setUp()
|
||||
self.auth = GenericPlugin()
|
||||
self.session = session.Session(auth=self.auth)
|
||||
|
||||
def test_setting_headers(self):
|
||||
text = uuid.uuid4().hex
|
||||
self.stub_url('GET', base_url=self.auth.url('prefix'), text=text)
|
||||
|
||||
resp = self.session.get('prefix', endpoint_filter=self.ENDPOINT_FILTER)
|
||||
|
||||
self.assertEqual(text, resp.text)
|
||||
|
||||
for k, v in six.iteritems(self.auth.headers):
|
||||
self.assertRequestHeaderEqual(k, v)
|
||||
|
||||
self.assertIsNone(self.session.get_token())
|
||||
self.assertEqual(self.auth.headers,
|
||||
self.session.get_auth_headers())
|
||||
self.assertNotIn('X-Auth-Token', self.requests.last_request.headers)
|
||||
|
@ -102,7 +102,8 @@ class V2IdentityPlugin(utils.TestCase):
|
||||
password=self.TEST_PASS)
|
||||
self.assertIsNone(a.user_id)
|
||||
s = session.Session(a)
|
||||
s.get_token()
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
req = {'auth': {'passwordCredentials': {'username': self.TEST_USER,
|
||||
'password': self.TEST_PASS}}}
|
||||
@ -117,7 +118,8 @@ class V2IdentityPlugin(utils.TestCase):
|
||||
password=self.TEST_PASS)
|
||||
self.assertIsNone(a.username)
|
||||
s = session.Session(a)
|
||||
s.get_token()
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
req = {'auth': {'passwordCredentials': {'userId': self.TEST_USER,
|
||||
'password': self.TEST_PASS}}}
|
||||
@ -132,7 +134,8 @@ class V2IdentityPlugin(utils.TestCase):
|
||||
password=self.TEST_PASS, tenant_id=self.TEST_TENANT_ID)
|
||||
self.assertIsNone(a.user_id)
|
||||
s = session.Session(a)
|
||||
s.get_token()
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
req = {'auth': {'passwordCredentials': {'username': self.TEST_USER,
|
||||
'password': self.TEST_PASS},
|
||||
@ -146,7 +149,8 @@ class V2IdentityPlugin(utils.TestCase):
|
||||
password=self.TEST_PASS, tenant_id=self.TEST_TENANT_ID)
|
||||
self.assertIsNone(a.username)
|
||||
s = session.Session(a)
|
||||
s.get_token()
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
req = {'auth': {'passwordCredentials': {'userId': self.TEST_USER,
|
||||
'password': self.TEST_PASS},
|
||||
@ -158,7 +162,8 @@ class V2IdentityPlugin(utils.TestCase):
|
||||
self.stub_auth(json=self.TEST_RESPONSE_DICT)
|
||||
a = v2.Token(self.TEST_URL, 'foo')
|
||||
s = session.Session(a)
|
||||
s.get_token()
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
req = {'auth': {'token': {'id': 'foo'}}}
|
||||
self.assertRequestBodyIs(json=req)
|
||||
@ -172,7 +177,8 @@ class V2IdentityPlugin(utils.TestCase):
|
||||
a = v2.Password(self.TEST_URL, username=self.TEST_USER,
|
||||
password=self.TEST_PASS, trust_id='trust')
|
||||
s = session.Session(a)
|
||||
s.get_token()
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
req = {'auth': {'passwordCredentials': {'username': self.TEST_USER,
|
||||
'password': self.TEST_PASS},
|
||||
@ -266,8 +272,11 @@ class V2IdentityPlugin(utils.TestCase):
|
||||
s = session.Session(auth=a)
|
||||
|
||||
self.assertEqual('token1', s.get_token())
|
||||
self.assertEqual({'X-Auth-Token': 'token1'}, s.get_auth_headers())
|
||||
|
||||
a.invalidate()
|
||||
self.assertEqual('token2', s.get_token())
|
||||
self.assertEqual({'X-Auth-Token': 'token2'}, s.get_auth_headers())
|
||||
|
||||
def test_doesnt_log_password(self):
|
||||
self.stub_auth(json=self.TEST_RESPONSE_DICT)
|
||||
@ -277,6 +286,8 @@ class V2IdentityPlugin(utils.TestCase):
|
||||
password=password)
|
||||
s = session.Session(auth=a)
|
||||
self.assertEqual(self.TEST_TOKEN, s.get_token())
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
self.assertNotIn(password, self.logger.output)
|
||||
|
||||
def test_password_with_no_user_id_or_name(self):
|
||||
|
@ -185,7 +185,8 @@ class V3IdentityPlugin(utils.TestCase):
|
||||
password=self.TEST_PASS)
|
||||
s = session.Session(auth=a)
|
||||
|
||||
s.get_token()
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
req = {'auth': {'identity':
|
||||
{'methods': ['password'],
|
||||
@ -225,7 +226,9 @@ class V3IdentityPlugin(utils.TestCase):
|
||||
a = v3.Password(self.TEST_URL, username=self.TEST_USER,
|
||||
password=self.TEST_PASS, domain_id=self.TEST_DOMAIN_ID)
|
||||
s = session.Session(a)
|
||||
s.get_token()
|
||||
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
req = {'auth': {'identity':
|
||||
{'methods': ['password'],
|
||||
@ -241,7 +244,9 @@ class V3IdentityPlugin(utils.TestCase):
|
||||
password=self.TEST_PASS,
|
||||
project_id=self.TEST_DOMAIN_ID)
|
||||
s = session.Session(a)
|
||||
s.get_token()
|
||||
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
req = {'auth': {'identity':
|
||||
{'methods': ['password'],
|
||||
@ -256,7 +261,9 @@ class V3IdentityPlugin(utils.TestCase):
|
||||
self.stub_auth(json=self.TEST_RESPONSE_DICT)
|
||||
a = v3.Token(self.TEST_URL, self.TEST_TOKEN)
|
||||
s = session.Session(auth=a)
|
||||
s.get_token()
|
||||
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
req = {'auth': {'identity':
|
||||
{'methods': ['token'],
|
||||
@ -279,7 +286,8 @@ class V3IdentityPlugin(utils.TestCase):
|
||||
a.auth_ref = access.AccessInfo.factory(body=d)
|
||||
s = session.Session(auth=a)
|
||||
|
||||
s.get_token()
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
self.assertEqual(a.auth_ref['expires_at'],
|
||||
self.TEST_RESPONSE_DICT['token']['expires_at'])
|
||||
@ -288,15 +296,20 @@ class V3IdentityPlugin(utils.TestCase):
|
||||
a = v3.Password(self.TEST_URL, username='username',
|
||||
password='password', project_id='project',
|
||||
domain_id='domain')
|
||||
|
||||
self.assertRaises(exceptions.AuthorizationFailure,
|
||||
a.get_token, None)
|
||||
self.assertRaises(exceptions.AuthorizationFailure,
|
||||
a.get_headers, None)
|
||||
|
||||
def test_with_trust_id(self):
|
||||
self.stub_auth(json=self.TEST_RESPONSE_DICT)
|
||||
a = v3.Password(self.TEST_URL, username=self.TEST_USER,
|
||||
password=self.TEST_PASS, trust_id='trust')
|
||||
s = session.Session(a)
|
||||
s.get_token()
|
||||
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
req = {'auth': {'identity':
|
||||
{'methods': ['password'],
|
||||
@ -312,7 +325,9 @@ class V3IdentityPlugin(utils.TestCase):
|
||||
t = v3.TokenMethod(token='foo')
|
||||
a = v3.Auth(self.TEST_URL, [p, t], trust_id='trust')
|
||||
s = session.Session(a)
|
||||
s.get_token()
|
||||
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
req = {'auth': {'identity':
|
||||
{'methods': ['password', 'token'],
|
||||
@ -331,7 +346,8 @@ class V3IdentityPlugin(utils.TestCase):
|
||||
a = v3.Auth(self.TEST_URL, [p, t], trust_id='trust')
|
||||
s = session.Session(auth=a)
|
||||
|
||||
s.get_token()
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
req = {'auth': {'identity':
|
||||
{'methods': ['password', 'token'],
|
||||
@ -438,8 +454,10 @@ class V3IdentityPlugin(utils.TestCase):
|
||||
s = session.Session(auth=a)
|
||||
|
||||
self.assertEqual('token1', s.get_token())
|
||||
self.assertEqual({'X-Auth-Token': 'token1'}, s.get_auth_headers())
|
||||
a.invalidate()
|
||||
self.assertEqual('token2', s.get_token())
|
||||
self.assertEqual({'X-Auth-Token': 'token2'}, s.get_auth_headers())
|
||||
|
||||
def test_doesnt_log_password(self):
|
||||
self.stub_auth(json=self.TEST_RESPONSE_DICT)
|
||||
@ -449,6 +467,8 @@ class V3IdentityPlugin(utils.TestCase):
|
||||
password=password)
|
||||
s = session.Session(a)
|
||||
self.assertEqual(self.TEST_TOKEN, s.get_token())
|
||||
self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
|
||||
s.get_auth_headers())
|
||||
|
||||
self.assertNotIn(password, self.logger.output)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user