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
|
||||
|
||||
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):
|
||||
"""Invalidate the current authentication data.
|
||||
|
||||
|
|
|
@ -97,6 +97,23 @@ class NoMatchingPlugin(ClientException):
|
|||
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):
|
||||
"""The response from the server is not valid for this request."""
|
||||
|
||||
|
|
|
@ -379,6 +379,19 @@ class Session(object):
|
|||
send = functools.partial(self._send_request,
|
||||
url, method, redirect, log, logger,
|
||||
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)
|
||||
|
||||
# handle getting a 401 Unauthorized response by invalidating the plugin
|
||||
|
@ -635,6 +648,59 @@ class Session(object):
|
|||
auth = self._auth_required(auth, msg)
|
||||
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):
|
||||
"""Invalidate an authentication plugin.
|
||||
|
||||
|
|
|
@ -14,12 +14,14 @@ import abc
|
|||
import datetime
|
||||
import uuid
|
||||
|
||||
import mock
|
||||
from oslo_utils import timeutils
|
||||
import six
|
||||
|
||||
from keystoneclient import access
|
||||
from keystoneclient.auth import base
|
||||
from keystoneclient.auth import identity
|
||||
from keystoneclient import exceptions
|
||||
from keystoneclient import fixture
|
||||
from keystoneclient import session
|
||||
from keystoneclient.tests.unit import utils
|
||||
|
@ -411,6 +413,9 @@ class GenericPlugin(base.BaseAuthPlugin):
|
|||
self.headers = {'headerA': 'valueA',
|
||||
'headerB': 'valueB'}
|
||||
|
||||
self.cert = '/path/to/cert'
|
||||
self.connection_params = {'cert': self.cert, 'verify': False}
|
||||
|
||||
def url(self, prefix):
|
||||
return '%s/%s' % (self.endpoint, prefix)
|
||||
|
||||
|
@ -424,6 +429,9 @@ class GenericPlugin(base.BaseAuthPlugin):
|
|||
def get_endpoint(self, session, **kwargs):
|
||||
return self.endpoint
|
||||
|
||||
def get_connection_params(self, session, **kwargs):
|
||||
return self.connection_params
|
||||
|
||||
|
||||
class GenericAuthPluginTests(utils.TestCase):
|
||||
|
||||
|
@ -451,3 +459,37 @@ class GenericAuthPluginTests(utils.TestCase):
|
|||
self.session.get_auth_headers())
|
||||
self.assertNotIn('X-Auth-Token',
|
||||
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