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:
Jamie Lennox 2014-12-12 11:55:02 +10:00
parent deeab3c164
commit 0ecf9b1ab5
4 changed files with 138 additions and 0 deletions

View File

@ -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.

View File

@ -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."""

View File

@ -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.

View File

@ -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))