diff --git a/nova/conf/__init__.py b/nova/conf/__init__.py index a152a9cb06f0..60d0ce04a2f3 100644 --- a/nova/conf/__init__.py +++ b/nova/conf/__init__.py @@ -60,6 +60,7 @@ from nova.conf import remote_debug from nova.conf import scheduler from nova.conf import serial_console from nova.conf import service +from nova.conf import service_token from nova.conf import servicegroup from nova.conf import spice from nova.conf import ssl @@ -114,6 +115,7 @@ rdp.register_opts(CONF) scheduler.register_opts(CONF) serial_console.register_opts(CONF) service.register_opts(CONF) +service_token.register_opts(CONF) servicegroup.register_opts(CONF) spice.register_opts(CONF) ssl.register_opts(CONF) diff --git a/nova/conf/service_token.py b/nova/conf/service_token.py new file mode 100644 index 000000000000..9c4d75959375 --- /dev/null +++ b/nova/conf/service_token.py @@ -0,0 +1,66 @@ +# 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 oslo_config import cfg + +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. + +Nova often reuses the user token provided to the nova-api to talk to other +REST APIs, such as Cinder. It is possible that while the +user token was valid when the request was made to Nova, the token may expire +before it reaches the other service. To avoid any failures, and to +make it clear it is Nova calling the service on the users behalf, we include +a server token along with the user token. Should the user's token have +expired, a valid service token ensures the REST API request will still be +accepted by the keystone middleware. + +This feature is currently experimental, and as such is turned off by default +while full testing and performance tuning of this feature is completed. +"""), +] + + +def register_opts(conf): + 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 list_opts(): + return { + service_user: ( + service_user_opts + + ks_loading.get_session_conf_options() + + ks_loading.get_auth_common_conf_options() + + ks_loading.get_auth_plugin_conf_options('password') + + ks_loading.get_auth_plugin_conf_options('v2password') + + ks_loading.get_auth_plugin_conf_options('v3password')) + } diff --git a/nova/service_auth.py b/nova/service_auth.py new file mode 100644 index 000000000000..b9191f303305 --- /dev/null +++ b/nova/service_auth.py @@ -0,0 +1,39 @@ +# 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 + +import nova.conf + + +CONF = nova.conf.CONF + +_SERVICE_AUTH = None + + +def get_auth_plugin(context): + 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= + nova.conf.service_token.SERVICE_USER_GROUP) + return service_token.ServiceTokenAuthWrapper( + user_auth=user_auth, + service_auth=_SERVICE_AUTH) + + return user_auth diff --git a/nova/tests/unit/test_service_auth.py b/nova/tests/unit/test_service_auth.py new file mode 100644 index 000000000000..481daf5a3243 --- /dev/null +++ b/nova/tests/unit/test_service_auth.py @@ -0,0 +1,47 @@ +# 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 +import mock + +import nova.conf +from nova import context +from nova import service_auth +from nova import test + + +CONF = nova.conf.CONF + + +class ServiceAuthTestCase(test.NoDBTestCase): + + def setUp(self): + super(ServiceAuthTestCase, self).setUp() + self.ctx = context.RequestContext('fake', 'fake') + + @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() + + def test_get_auth_plugin_wraps(self): + self.flags(send_service_user_token=True, group='service_user') + + result = service_auth.get_auth_plugin(self.ctx) + + self.assertIsInstance(result, service_token.ServiceTokenAuthWrapper) diff --git a/nova/volume/cinder.py b/nova/volume/cinder.py index addb43726fde..019956275856 100644 --- a/nova/volume/cinder.py +++ b/nova/volume/cinder.py @@ -39,6 +39,7 @@ from nova import exception from nova.i18n import _ from nova.i18n import _LE from nova.i18n import _LW +from nova import service_auth CONF = nova.conf.CONF @@ -65,7 +66,7 @@ def cinderclient(context): url = None endpoint_override = None - auth = context.get_auth_plugin() + auth = service_auth.get_auth_plugin(context) service_type, service_name, interface = CONF.cinder.catalog_info.split(':') service_parameters = {'service_type': service_type, diff --git a/releasenotes/notes/validate-expired-user-tokens-57a265cb4ee4ba6f.yaml b/releasenotes/notes/validate-expired-user-tokens-57a265cb4ee4ba6f.yaml new file mode 100644 index 000000000000..10ccb6983f26 --- /dev/null +++ b/releasenotes/notes/validate-expired-user-tokens-57a265cb4ee4ba6f.yaml @@ -0,0 +1,14 @@ +--- +features: + - Added support for Keystone middleware feature where if service token is + sent along with the user token, then it will ignore the expiration of user + token. This helps deal with issues of user tokens expiring during long + running operations, such as live-migration where nova tries to access + Cinder at the end of the operation using the user token that has expired. + In order to use this functionality a service user needs to be created. + Add service user configurations in ``nova.conf`` under + ``service_user`` group and set ``send_service_user_token`` flag to + ``True``. The minimum Keytone API version 3.8 and Keystone middleware + version 4.12.0 is required to use this functionality. + This only currently works with nova - cinder API interactions. +