From 0787710c2477b127baeba901dc0a1598f7f1eabe Mon Sep 17 00:00:00 2001 From: nirajsingh Date: Wed, 8 Nov 2017 14:07:14 +0530 Subject: [PATCH] Add service_token for cinder-nova interaction Service token will be passed along with user token to communicate with services when dealing with long running tasks like Create volume snapshot. Partial-Implements: blueprint use-service-tokens Change-Id: Id95beae0a46ab492756e0108039fefb28f4f0b69 --- cinder/compute/nova.py | 4 + cinder/context.py | 36 ++++++++- cinder/exception.py | 6 ++ cinder/opts.py | 5 ++ cinder/service_auth.py | 73 ++++++++++++++++++ cinder/tests/unit/test_service_auth.py | 77 +++++++++++++++++++ ...-expired-user-tokens-40b15322197653ae.yaml | 9 +++ 7 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 cinder/service_auth.py create mode 100644 cinder/tests/unit/test_service_auth.py create mode 100644 releasenotes/notes/validate-expired-user-tokens-40b15322197653ae.yaml diff --git a/cinder/compute/nova.py b/cinder/compute/nova.py index bd264405f2f..e54b748ef65 100644 --- a/cinder/compute/nova.py +++ b/cinder/compute/nova.py @@ -27,6 +27,7 @@ from requests import exceptions as request_exceptions from cinder.db import base from cinder import exception +from cinder import service_auth nova_opts = [ cfg.StrOpt('region_name', @@ -107,6 +108,9 @@ def novaclient(context, privileged_user=False, timeout=None, api_version=None): project_name=context.project_name, project_domain_id=context.project_domain_id) + if CONF.auth_strategy == 'keystone': + n_auth = service_auth.get_auth_plugin(context, auth=n_auth) + keystone_session = ks_loading.load_session_from_conf_options( CONF, NOVA_GROUP, diff --git a/cinder/context.py b/cinder/context.py index ca693876127..ef681a22b2f 100644 --- a/cinder/context.py +++ b/cinder/context.py @@ -19,6 +19,8 @@ import copy +from keystoneauth1.access import service_catalog as ksa_service_catalog +from keystoneauth1 import plugin from oslo_config import cfg from oslo_context import context from oslo_db.sqlalchemy import enginefacade @@ -46,6 +48,31 @@ CONF.register_opts(context_opts) LOG = logging.getLogger(__name__) +class _ContextAuthPlugin(plugin.BaseAuthPlugin): + """A keystoneauth auth plugin that uses the values from the Context. + + Ideally we would use the plugin provided by auth_token middleware however + this plugin isn't serialized yet so we construct one from the serialized + auth data. + """ + + def __init__(self, auth_token, sc): + super(_ContextAuthPlugin, self).__init__() + + self.auth_token = auth_token + self.service_catalog = ksa_service_catalog.ServiceCatalogV2(sc) + + def get_token(self, *args, **kwargs): + return self.auth_token + + def get_endpoint(self, session, service_type=None, interface=None, + region_name=None, service_name=None, **kwargs): + return self.service_catalog.url_for(service_type=service_type, + service_name=service_name, + interface=interface, + region_name=region_name) + + @enginefacade.transaction_context_provider class RequestContext(context.RequestContext): """Security context and request information. @@ -56,7 +83,7 @@ class RequestContext(context.RequestContext): def __init__(self, user_id=None, project_id=None, is_admin=None, read_deleted="no", project_name=None, remote_address=None, timestamp=None, quota_class=None, service_catalog=None, - **kwargs): + user_auth_plugin=None, **kwargs): """Initialize RequestContext. :param read_deleted: 'no' indicates deleted records are hidden, 'yes' @@ -100,6 +127,13 @@ class RequestContext(context.RequestContext): self.is_admin = policy.check_is_admin(self) elif self.is_admin and 'admin' not in self.roles: self.roles.append('admin') + self.user_auth_plugin = user_auth_plugin + + def get_auth_plugin(self): + if self.user_auth_plugin: + return self.user_auth_plugin + else: + return _ContextAuthPlugin(self.auth_token, self.service_catalog) def _get_read_deleted(self): return self._read_deleted diff --git a/cinder/exception.py b/cinder/exception.py index 6e39ae15ed2..eee06421bf2 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -1344,3 +1344,9 @@ class GPFSDriverUnsupportedOperation(VolumeBackendAPIException): class InvalidName(Invalid): message = _("An invalid 'name' value was provided. %(reason)s") + + +class ServiceUserTokenNoAuth(CinderException): + message = _("The [service_user] send_service_user_token option was " + "requested, but no service auth could be loaded. Please check " + "the [service_user] configuration section.") diff --git a/cinder/opts.py b/cinder/opts.py index 4b64638e092..4c30f63833a 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -66,6 +66,7 @@ from cinder.scheduler.weights import capacity as \ from cinder.scheduler.weights import volume_number as \ cinder_scheduler_weights_volumenumber from cinder import service as cinder_service +from cinder import service_auth as cinder_serviceauth from cinder import ssh_utils as cinder_sshutils from cinder.transfer import api as cinder_transfer_api from cinder.volume import api as cinder_volume_api @@ -269,6 +270,10 @@ def list_opts(): itertools.chain( cinder_keymgr_confkeymgr.key_mgr_opts, )), + ('service_user', + itertools.chain( + cinder_serviceauth.service_user_opts, + )), ('backend_defaults', itertools.chain( cinder_volume_driver.volume_opts, diff --git a/cinder/service_auth.py b/cinder/service_auth.py new file mode 100644 index 00000000000..6c4aaf809db --- /dev/null +++ b/cinder/service_auth.py @@ -0,0 +1,73 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from keystoneauth1 import loading as ks_loading +from keystoneauth1 import service_token +from oslo_config import cfg + +from cinder import exception + +CONF = cfg.CONF +_SERVICE_AUTH = None + +SERVICE_USER_GROUP = 'service_user' + +service_user = cfg.OptGroup( + SERVICE_USER_GROUP, + title='Service token authentication type options', + help=""" +Configuration options for service to service authentication using a service +token. These options allow to send a service token along with the +user's token when contacting external REST APIs. +""" +) +service_user_opts = [ + cfg.BoolOpt('send_service_user_token', + default=False, + help=""" +When True, if sending a user token to an REST API, also send a service token. +""") +] + +CONF.register_group(service_user) +CONF.register_opts(service_user_opts, group=service_user) + +ks_loading.register_session_conf_options(CONF, SERVICE_USER_GROUP) +ks_loading.register_auth_conf_options(CONF, SERVICE_USER_GROUP) + + +def reset_globals(): + """For async unit test consistency.""" + global _SERVICE_AUTH + _SERVICE_AUTH = None + + +def get_auth_plugin(context, auth=None): + if auth: + user_auth = auth + else: + user_auth = context.get_auth_plugin() + + if CONF.service_user.send_service_user_token: + global _SERVICE_AUTH + if not _SERVICE_AUTH: + _SERVICE_AUTH = ks_loading.load_auth_from_conf_options( + CONF, group=SERVICE_USER_GROUP) + if _SERVICE_AUTH is None: + # This can happen if no auth_type is specified, which probably + # means there's no auth information in the [service_user] group + raise exception.ServiceUserTokenNoAuth() + return service_token.ServiceTokenAuthWrapper( + user_auth=user_auth, service_auth=_SERVICE_AUTH) + + return user_auth diff --git a/cinder/tests/unit/test_service_auth.py b/cinder/tests/unit/test_service_auth.py new file mode 100644 index 00000000000..81394da79a0 --- /dev/null +++ b/cinder/tests/unit/test_service_auth.py @@ -0,0 +1,77 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneauth1.identity.generic import password +from keystoneauth1 import loading as ks_loading +from keystoneauth1 import service_token +import mock + +from cinder import context +from cinder import exception +from cinder import service_auth +from cinder import test + +from oslo_config import cfg + +CONF = cfg.CONF + + +class ServiceAuthTestCase(test.TestCase): + + def setUp(self): + super(ServiceAuthTestCase, self).setUp() + self.ctx = context.RequestContext('fake', 'fake') + service_auth.reset_globals() + + @mock.patch.object(ks_loading, 'load_auth_from_conf_options') + def test_get_auth_plugin_no_wraps(self, mock_load): + context = mock.MagicMock() + context.get_auth_plugin.return_value = "fake" + + result = service_auth.get_auth_plugin(context) + + self.assertEqual("fake", result) + mock_load.assert_not_called() + + @mock.patch.object(ks_loading, 'load_auth_from_conf_options') + def test_get_auth_plugin_wraps(self, mock_load): + self.flags(send_service_user_token=True, group='service_user') + result = service_auth.get_auth_plugin(self.ctx) + + self.assertIsInstance(result, service_token.ServiceTokenAuthWrapper) + mock_load.assert_called_once_with(mock.ANY, group='service_user') + + def test_service_auth_requested_but_no_auth_given(self): + self.flags(send_service_user_token=True, group='service_user') + + self.assertRaises(exception.ServiceUserTokenNoAuth, + service_auth.get_auth_plugin, self.ctx) + + @mock.patch.object(ks_loading, 'load_auth_from_conf_options') + def test_get_auth_plugin_with_auth(self, mock_load): + self.flags(send_service_user_token=True, group='service_user') + + mock_load.return_value = password.Password + result = service_auth.get_auth_plugin( + self.ctx, auth=mock_load.return_value) + + self.assertEqual(mock_load.return_value, result.user_auth) + self.assertIsInstance(result, service_token.ServiceTokenAuthWrapper) + mock_load.assert_called_once_with(mock.ANY, group='service_user') + + def test_get_auth_plugin_with_auth_and_service_token_false(self): + self.flags(send_service_user_token=False, group='service_user') + + n_auth = password.Password + result = service_auth.get_auth_plugin(self.ctx, auth=n_auth) + + self.assertEqual(n_auth, result) diff --git a/releasenotes/notes/validate-expired-user-tokens-40b15322197653ae.yaml b/releasenotes/notes/validate-expired-user-tokens-40b15322197653ae.yaml new file mode 100644 index 00000000000..433f3952712 --- /dev/null +++ b/releasenotes/notes/validate-expired-user-tokens-40b15322197653ae.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added support for Keystone middleware feature to pass service token along with the + user token for Cinder to Nova interaction. This will help get rid of user token + expiration issues during long running tasks e.g. creating volume snapshot. + To use this functionality a service user needs to be created first. Add the service + user configurations in ``cinder.conf`` under ``service_user`` group and set + ``send_service_user_token`` flag to ``True``.