Add get_communication_params interface to plugins
To allow authentication plugins such as using client certificates or doing kerberos authentication with every request we need a way for the plugins to manipulate the send parameters. Change-Id: Ib9e81773ab988ea05869bc27097d2b25e963e59c Blueprint: generic-plugins
This commit is contained in:
parent
deeab3c164
commit
0ecf9b1ab5
|
@ -168,6 +168,19 @@ class BaseAuthPlugin(object):
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_connection_params(self, session, **kwargs):
|
||||||
|
"""Return any additional connection parameters required for the plugin.
|
||||||
|
|
||||||
|
: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
|
||||||
|
"""
|
||||||
|
return {}
|
||||||
|
|
||||||
def invalidate(self):
|
def invalidate(self):
|
||||||
"""Invalidate the current authentication data.
|
"""Invalidate the current authentication data.
|
||||||
|
|
||||||
|
|
|
@ -97,6 +97,23 @@ class NoMatchingPlugin(ClientException):
|
||||||
super(NoMatchingPlugin, self).__init__(msg)
|
super(NoMatchingPlugin, self).__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedParameters(ClientException):
|
||||||
|
"""A parameter that was provided or returned is not supported.
|
||||||
|
|
||||||
|
:param list(str) names: Names of the unsupported parameters.
|
||||||
|
|
||||||
|
.. py:attribute:: names
|
||||||
|
|
||||||
|
Names of the unsupported parameters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, names):
|
||||||
|
self.names = names
|
||||||
|
|
||||||
|
m = _('The following parameters were given that are unsupported: %s')
|
||||||
|
super(UnsupportedParameters, self).__init__(m % ', '.join(self.names))
|
||||||
|
|
||||||
|
|
||||||
class InvalidResponse(ClientException):
|
class InvalidResponse(ClientException):
|
||||||
"""The response from the server is not valid for this request."""
|
"""The response from the server is not valid for this request."""
|
||||||
|
|
||||||
|
|
|
@ -379,6 +379,19 @@ class Session(object):
|
||||||
send = functools.partial(self._send_request,
|
send = functools.partial(self._send_request,
|
||||||
url, method, redirect, log, logger,
|
url, method, redirect, log, logger,
|
||||||
connect_retries)
|
connect_retries)
|
||||||
|
|
||||||
|
try:
|
||||||
|
connection_params = self.get_auth_connection_params(auth=auth)
|
||||||
|
except exceptions.MissingAuthPlugin:
|
||||||
|
# NOTE(jamielennox): If we've gotten this far without an auth
|
||||||
|
# plugin then we should be happy with allowing no additional
|
||||||
|
# connection params. This will be the typical case for plugins
|
||||||
|
# anyway.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if connection_params:
|
||||||
|
kwargs.update(connection_params)
|
||||||
|
|
||||||
resp = send(**kwargs)
|
resp = send(**kwargs)
|
||||||
|
|
||||||
# handle getting a 401 Unauthorized response by invalidating the plugin
|
# handle getting a 401 Unauthorized response by invalidating the plugin
|
||||||
|
@ -635,6 +648,59 @@ class Session(object):
|
||||||
auth = self._auth_required(auth, msg)
|
auth = self._auth_required(auth, msg)
|
||||||
return auth.get_endpoint(self, **kwargs)
|
return auth.get_endpoint(self, **kwargs)
|
||||||
|
|
||||||
|
def get_auth_connection_params(self, auth=None, **kwargs):
|
||||||
|
"""Return auth connection params as provided by the auth plugin.
|
||||||
|
|
||||||
|
An auth plugin may specify connection parameters to the request like
|
||||||
|
providing a client certificate for communication.
|
||||||
|
|
||||||
|
We restrict the values that may be returned from this function to
|
||||||
|
prevent an auth plugin overriding values unrelated to connection
|
||||||
|
parmeters. The values that are currently accepted are:
|
||||||
|
|
||||||
|
- `cert`: a path to a client certificate, or tuple of client
|
||||||
|
certificate and key pair that are used with this request.
|
||||||
|
- `verify`: a boolean value to indicate verifying SSL certificates
|
||||||
|
against the system CAs or a path to a CA file to verify with.
|
||||||
|
|
||||||
|
These values are passed to the requests library and further information
|
||||||
|
on accepted values may be found there.
|
||||||
|
|
||||||
|
:param auth: The auth plugin to use for tokens. Overrides the plugin
|
||||||
|
on the session. (optional)
|
||||||
|
:type auth: keystoneclient.auth.base.BaseAuthPlugin
|
||||||
|
|
||||||
|
:raises keystoneclient.exceptions.AuthorizationFailure: if a new token
|
||||||
|
fetch fails.
|
||||||
|
:raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not
|
||||||
|
available.
|
||||||
|
:raises keystoneclient.exceptions.UnsupportedParameters: if the plugin
|
||||||
|
returns a parameter that is not supported by this session.
|
||||||
|
|
||||||
|
:returns: Authentication headers or None for failure.
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
msg = _('An auth plugin is required to fetch connection params')
|
||||||
|
auth = self._auth_required(auth, msg)
|
||||||
|
params = auth.get_connection_params(self, **kwargs)
|
||||||
|
|
||||||
|
# NOTE(jamielennox): There needs to be some consensus on what
|
||||||
|
# parameters are allowed to be modified by the auth plugin here.
|
||||||
|
# Ideally I think it would be only the send() parts of the request
|
||||||
|
# flow. For now lets just allow certain elements.
|
||||||
|
params_copy = params.copy()
|
||||||
|
|
||||||
|
for arg in ('cert', 'verify'):
|
||||||
|
try:
|
||||||
|
kwargs[arg] = params_copy.pop(arg)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if params_copy:
|
||||||
|
raise exceptions.UnsupportedParameters(list(params_copy.keys()))
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
def invalidate(self, auth=None):
|
def invalidate(self, auth=None):
|
||||||
"""Invalidate an authentication plugin.
|
"""Invalidate an authentication plugin.
|
||||||
|
|
||||||
|
|
|
@ -14,12 +14,14 @@ import abc
|
||||||
import datetime
|
import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import mock
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from keystoneclient import access
|
from keystoneclient import access
|
||||||
from keystoneclient.auth import base
|
from keystoneclient.auth import base
|
||||||
from keystoneclient.auth import identity
|
from keystoneclient.auth import identity
|
||||||
|
from keystoneclient import exceptions
|
||||||
from keystoneclient import fixture
|
from keystoneclient import fixture
|
||||||
from keystoneclient import session
|
from keystoneclient import session
|
||||||
from keystoneclient.tests.unit import utils
|
from keystoneclient.tests.unit import utils
|
||||||
|
@ -411,6 +413,9 @@ class GenericPlugin(base.BaseAuthPlugin):
|
||||||
self.headers = {'headerA': 'valueA',
|
self.headers = {'headerA': 'valueA',
|
||||||
'headerB': 'valueB'}
|
'headerB': 'valueB'}
|
||||||
|
|
||||||
|
self.cert = '/path/to/cert'
|
||||||
|
self.connection_params = {'cert': self.cert, 'verify': False}
|
||||||
|
|
||||||
def url(self, prefix):
|
def url(self, prefix):
|
||||||
return '%s/%s' % (self.endpoint, prefix)
|
return '%s/%s' % (self.endpoint, prefix)
|
||||||
|
|
||||||
|
@ -424,6 +429,9 @@ class GenericPlugin(base.BaseAuthPlugin):
|
||||||
def get_endpoint(self, session, **kwargs):
|
def get_endpoint(self, session, **kwargs):
|
||||||
return self.endpoint
|
return self.endpoint
|
||||||
|
|
||||||
|
def get_connection_params(self, session, **kwargs):
|
||||||
|
return self.connection_params
|
||||||
|
|
||||||
|
|
||||||
class GenericAuthPluginTests(utils.TestCase):
|
class GenericAuthPluginTests(utils.TestCase):
|
||||||
|
|
||||||
|
@ -451,3 +459,37 @@ class GenericAuthPluginTests(utils.TestCase):
|
||||||
self.session.get_auth_headers())
|
self.session.get_auth_headers())
|
||||||
self.assertNotIn('X-Auth-Token',
|
self.assertNotIn('X-Auth-Token',
|
||||||
self.requests_mock.last_request.headers)
|
self.requests_mock.last_request.headers)
|
||||||
|
|
||||||
|
def test_setting_connection_params(self):
|
||||||
|
text = uuid.uuid4().hex
|
||||||
|
|
||||||
|
with mock.patch.object(self.session.session, 'request') as mocked:
|
||||||
|
mocked.return_value = utils.TestResponse({'status_code': 200,
|
||||||
|
'text': text})
|
||||||
|
resp = self.session.get('prefix',
|
||||||
|
endpoint_filter=self.ENDPOINT_FILTER)
|
||||||
|
|
||||||
|
self.assertEqual(text, resp.text)
|
||||||
|
|
||||||
|
# the cert and verify values passed to request are those that were
|
||||||
|
# returned from the auth plugin as connection params.
|
||||||
|
|
||||||
|
mocked.assert_called_once_with('GET',
|
||||||
|
self.auth.url('prefix'),
|
||||||
|
headers=mock.ANY,
|
||||||
|
allow_redirects=False,
|
||||||
|
cert=self.auth.cert,
|
||||||
|
verify=False)
|
||||||
|
|
||||||
|
def test_setting_bad_connection_params(self):
|
||||||
|
# The uuid name parameter here is unknown and not in the allowed params
|
||||||
|
# to be returned to the session and so an error will be raised.
|
||||||
|
name = uuid.uuid4().hex
|
||||||
|
self.auth.connection_params[name] = uuid.uuid4().hex
|
||||||
|
|
||||||
|
e = self.assertRaises(exceptions.UnsupportedParameters,
|
||||||
|
self.session.get,
|
||||||
|
'prefix',
|
||||||
|
endpoint_filter=self.ENDPOINT_FILTER)
|
||||||
|
|
||||||
|
self.assertIn(name, str(e))
|
||||||
|
|
Loading…
Reference in New Issue