From 1b782cee8552ec02f7303ee6f9ba9d1f2c180d07 Mon Sep 17 00:00:00 2001 From: kairat_kushaev Date: Tue, 1 Dec 2015 14:18:02 +0300 Subject: [PATCH] Implement re-authentication for swift driver Enable re-authentication when downloading or uploading images. If single tenant store is used then request the new token for service user. If multi tenant store is used then request the new token with trusts. Note: Both features are available for Keystone V3 API only. If store.auth_version is not '3' then use old approach to receive Swift Connections. DocImpact: Describe how to enable/disable re-authentication and add notes about Keystone v3 support only. Implements bp prevention-of-401-in-swift-driver Change-Id: Id4e479e29ae8f71ff93f769246989b4b180f5c68 --- glance_store/_drivers/swift/store.py | 402 +++++++++++------- glance_store/tests/unit/test_swift_store.py | 128 +++++- ...-unauthorized-errors-ebb9cf2236595cd0.yaml | 12 + 3 files changed, 398 insertions(+), 144 deletions(-) create mode 100644 releasenotes/notes/prevent-unauthorized-errors-ebb9cf2236595cd0.yaml diff --git a/glance_store/_drivers/swift/store.py b/glance_store/_drivers/swift/store.py index 536d34c0..c32d621c 100644 --- a/glance_store/_drivers/swift/store.py +++ b/glance_store/_drivers/swift/store.py @@ -32,6 +32,10 @@ try: except ImportError: swiftclient = None +from keystoneclient.auth.identity import v3 as ks_v3 +from keystoneclient import session as ks_session +from keystoneclient.v3 import client as ks_client + import glance_store from glance_store._drivers.swift import connection_manager from glance_store._drivers.swift import utils as sutils @@ -139,7 +143,7 @@ _SWIFT_OPTS = [ ] -def swift_retry_iter(resp_iter, length, store, location, context): +def swift_retry_iter(resp_iter, length, store, location, manager): if not length and isinstance(resp_iter, six.BytesIO): if six.PY3: # On Python 3, io.BytesIO does not have a len attribute, instead @@ -184,9 +188,9 @@ def swift_retry_iter(resp_iter, length, store, location, context): 'max_retries': retry_count, 'start': bytes_read, 'end': length}) - (_resp_headers, resp_iter) = store._get_object(location, None, - bytes_read, - context=context) + (_resp_headers, resp_iter) = store._get_object(location, + manager, + bytes_read) else: break @@ -439,16 +443,14 @@ class BaseStore(driver.Store): reason=msg) super(BaseStore, self).configure(re_raise_bsc=re_raise_bsc) - def _get_object(self, location, connection=None, start=None, context=None): - if not connection: - connection = self.get_connection(location, context=context) + def _get_object(self, location, manager, start=None): headers = {} if start is not None: bytes_range = 'bytes=%d-' % start headers = {'Range': bytes_range} try: - resp_headers, resp_body = connection.get_object( + resp_headers, resp_body = manager.get_connection().get_object( location.container, location.obj, resp_chunk_size=self.CHUNKSIZE, headers=headers) except swiftclient.ClientException as e: @@ -465,21 +467,26 @@ class BaseStore(driver.Store): def get(self, location, connection=None, offset=0, chunk_size=None, context=None): location = location.store_location - (resp_headers, resp_body) = self._get_object(location, connection, - context=context) + # initialize manager to receive valid connections + allow_retry = \ + self.conf.glance_store.swift_store_retry_get_count > 0 + with get_manager_for_store(self, location, context, + allow_reauth=allow_retry) as manager: + (resp_headers, resp_body) = self._get_object(location, + manager=manager) - class ResponseIndexable(glance_store.Indexable): - def another(self): - try: - return next(self.wrapped) - except StopIteration: - return '' + class ResponseIndexable(glance_store.Indexable): + def another(self): + try: + return next(self.wrapped) + except StopIteration: + return '' - length = int(resp_headers.get('content-length', 0)) - if self.conf.glance_store.swift_store_retry_get_count > 0: - resp_body = swift_retry_iter(resp_body, length, - self, location, context) - return (ResponseIndexable(resp_body, length), length) + length = int(resp_headers.get('content-length', 0)) + if allow_retry: + resp_body = swift_retry_iter(resp_body, length, + self, location, manager=manager) + return ResponseIndexable(resp_body, length), length def get_size(self, location, connection=None, context=None): location = location.store_location @@ -515,138 +522,143 @@ class BaseStore(driver.Store): @capabilities.check def add(self, image_id, image_file, image_size, - connection=None, context=None, verifier=None): + context=None, verifier=None): location = self.create_location(image_id, context=context) - if not connection: - connection = self.get_connection(location, context=context) + # initialize a manager with re-auth if image need to be splitted + need_chunks = (image_size == 0) or ( + image_size >= self.large_object_size) + with get_manager_for_store(self, location, context, + allow_reauth=need_chunks) as manager: - self._create_container_if_missing(location.container, connection) + self._create_container_if_missing(location.container, + manager.get_connection()) - LOG.debug("Adding image object '%(obj_name)s' " - "to Swift" % dict(obj_name=location.obj)) - try: - if image_size > 0 and image_size < self.large_object_size: - # Image size is known, and is less than large_object_size. - # Send to Swift with regular PUT. - if verifier: - checksum = hashlib.md5() - reader = ChunkReader(image_file, checksum, - image_size, verifier) - obj_etag = connection.put_object(location.container, - location.obj, - reader, - content_length=image_size) - else: - obj_etag = connection.put_object(location.container, - location.obj, image_file, - content_length=image_size) - else: - # Write the image into Swift in chunks. - chunk_id = 1 - if image_size > 0: - total_chunks = str(int( - math.ceil(float(image_size) / - float(self.large_object_chunk_size)))) - else: - # image_size == 0 is when we don't know the size - # of the image. This can occur with older clients - # that don't inspect the payload size. - LOG.debug("Cannot determine image size. Adding as a " - "segmented object to Swift.") - total_chunks = '?' - - checksum = hashlib.md5() - written_chunks = [] - combined_chunks_size = 0 - while True: - chunk_size = self.large_object_chunk_size - if image_size == 0: - content_length = None + LOG.debug("Adding image object '%(obj_name)s' " + "to Swift" % dict(obj_name=location.obj)) + try: + if not need_chunks: + # Image size is known, and is less than large_object_size. + # Send to Swift with regular PUT. + if verifier: + checksum = hashlib.md5() + reader = ChunkReader(image_file, checksum, + image_size, verifier) + obj_etag = manager.get_connection().put_object( + location.container, location.obj, + reader, content_length=image_size) else: - left = image_size - combined_chunks_size - if left == 0: + obj_etag = manager.get_connection().put_object( + location.container, location.obj, + image_file, content_length=image_size) + else: + # Write the image into Swift in chunks. + chunk_id = 1 + if image_size > 0: + total_chunks = str(int( + math.ceil(float(image_size) / + float(self.large_object_chunk_size)))) + else: + # image_size == 0 is when we don't know the size + # of the image. This can occur with older clients + # that don't inspect the payload size. + LOG.debug("Cannot determine image size. Adding as a " + "segmented object to Swift.") + total_chunks = '?' + + checksum = hashlib.md5() + written_chunks = [] + combined_chunks_size = 0 + while True: + chunk_size = self.large_object_chunk_size + if image_size == 0: + content_length = None + else: + left = image_size - combined_chunks_size + if left == 0: + break + if chunk_size > left: + chunk_size = left + content_length = chunk_size + + chunk_name = "%s-%05d" % (location.obj, chunk_id) + reader = ChunkReader(image_file, checksum, chunk_size, + verifier) + if reader.is_zero_size is True: + LOG.debug('Not writing zero-length chunk.') break - if chunk_size > left: - chunk_size = left - content_length = chunk_size + try: + chunk_etag = manager.get_connection().put_object( + location.container, chunk_name, reader, + content_length=content_length) + written_chunks.append(chunk_name) + except Exception: + # Delete orphaned segments from swift backend + with excutils.save_and_reraise_exception(): + LOG.exception(_("Error during chunked upload " + "to backend, deleting stale " + "chunks")) + self._delete_stale_chunks( + manager.get_connection(), + location.container, + written_chunks) - chunk_name = "%s-%05d" % (location.obj, chunk_id) - reader = ChunkReader(image_file, checksum, chunk_size, - verifier) - if reader.is_zero_size is True: - LOG.debug('Not writing zero-length chunk.') - break - try: - chunk_etag = connection.put_object( - location.container, chunk_name, reader, - content_length=content_length) - written_chunks.append(chunk_name) - except Exception: - # Delete orphaned segments from swift backend - with excutils.save_and_reraise_exception(): - LOG.exception(_("Error during chunked upload to " - "backend, deleting stale chunks")) - self._delete_stale_chunks(connection, - location.container, - written_chunks) + bytes_read = reader.bytes_read + msg = ("Wrote chunk %(chunk_name)s (%(chunk_id)d/" + "%(total_chunks)s) of length %(bytes_read)d " + "to Swift returning MD5 of content: " + "%(chunk_etag)s" % + {'chunk_name': chunk_name, + 'chunk_id': chunk_id, + 'total_chunks': total_chunks, + 'bytes_read': bytes_read, + 'chunk_etag': chunk_etag}) + LOG.debug(msg) - bytes_read = reader.bytes_read - msg = ("Wrote chunk %(chunk_name)s (%(chunk_id)d/" - "%(total_chunks)s) of length %(bytes_read)d " - "to Swift returning MD5 of content: " - "%(chunk_etag)s" % - {'chunk_name': chunk_name, - 'chunk_id': chunk_id, - 'total_chunks': total_chunks, - 'bytes_read': bytes_read, - 'chunk_etag': chunk_etag}) - LOG.debug(msg) + chunk_id += 1 + combined_chunks_size += bytes_read - chunk_id += 1 - combined_chunks_size += bytes_read + # In the case we have been given an unknown image size, + # set the size to the total size of the combined chunks. + if image_size == 0: + image_size = combined_chunks_size - # In the case we have been given an unknown image size, - # set the size to the total size of the combined chunks. - if image_size == 0: - image_size = combined_chunks_size + # Now we write the object manifest and return the + # manifest's etag... + manifest = "%s/%s-" % (location.container, location.obj) + headers = {'ETag': hashlib.md5(b"").hexdigest(), + 'X-Object-Manifest': manifest} - # Now we write the object manifest and return the - # manifest's etag... - manifest = "%s/%s-" % (location.container, location.obj) - headers = {'ETag': hashlib.md5(b"").hexdigest(), - 'X-Object-Manifest': manifest} + # The ETag returned for the manifest is actually the + # MD5 hash of the concatenated checksums of the strings + # of each chunk...so we ignore this result in favour of + # the MD5 of the entire image file contents, so that + # users can verify the image file contents accordingly + manager.get_connection().put_object(location.container, + location.obj, + None, headers=headers) + obj_etag = checksum.hexdigest() - # The ETag returned for the manifest is actually the - # MD5 hash of the concatenated checksums of the strings - # of each chunk...so we ignore this result in favour of - # the MD5 of the entire image file contents, so that - # users can verify the image file contents accordingly - connection.put_object(location.container, location.obj, - None, headers=headers) - obj_etag = checksum.hexdigest() + # NOTE: We return the user and key here! Have to because + # location is used by the API server to return the actual + # image data. We *really* should consider NOT returning + # the location attribute from GET /images/ and + # GET /images/details + if sutils.is_multiple_swift_store_accounts_enabled(self.conf): + include_creds = False + else: + include_creds = True + return (location.get_uri(credentials_included=include_creds), + image_size, obj_etag, {}) + except swiftclient.ClientException as e: + if e.http_status == http_client.CONFLICT: + msg = _("Swift already has an image at this location") + raise exceptions.Duplicate(message=msg) - # NOTE: We return the user and key here! Have to because - # location is used by the API server to return the actual - # image data. We *really* should consider NOT returning - # the location attribute from GET /images/ and - # GET /images/details - if sutils.is_multiple_swift_store_accounts_enabled(self.conf): - include_creds = False - else: - include_creds = True - - return (location.get_uri(credentials_included=include_creds), - image_size, obj_etag, {}) - except swiftclient.ClientException as e: - if e.http_status == http_client.CONFLICT: - msg = _("Swift already has an image at this location") - raise exceptions.Duplicate(message=msg) - - msg = (_(u"Failed to add object to Swift.\n" - "Got error from Swift: %s.") - % encodeutils.exception_to_unicode(e)) - LOG.error(msg) - raise glance_store.BackendException(msg) + msg = (_(u"Failed to add object to Swift.\n" + "Got error from Swift: %s.") + % encodeutils.exception_to_unicode(e)) + LOG.error(msg) + raise glance_store.BackendException(msg) @capabilities.check def delete(self, location, connection=None, context=None): @@ -765,7 +777,13 @@ class BaseStore(driver.Store): :return: swiftclient connection that allows to request container and others """ - raise NotImplementedError() + # initialize a connection + return swiftclient.Connection( + preauthurl=storage_url, + preauthtoken=auth_token, + insecure=self.insecure, + ssl_compression=self.ssl_compression, + cacert=self.cacert) class SingleTenantStore(BaseStore): @@ -904,6 +922,39 @@ class SingleTenantStore(BaseStore): auth_version=self.auth_version, os_options=os_options, ssl_compression=self.ssl_compression, cacert=self.cacert) + def init_client(self, location, context=None): + """Initialize keystone client with swift service user credentials""" + # prepare swift admin credentials + if not location.user: + reason = _("Location is missing user:password information.") + LOG.info(reason) + raise exceptions.BadStoreUri(message=reason) + + auth_url = location.swift_url + if not auth_url.endswith('/'): + auth_url += '/' + + try: + tenant_name, user = location.user.split(':') + except ValueError: + reason = (_("Badly formed tenant:user '%(user)s' in " + "Swift URI") % {'user': location.user}) + LOG.info(reason) + raise exceptions.BadStoreUri(message=reason) + + # initialize a keystone plugin for swift admin with creds + password = ks_v3.Password(auth_url=auth_url, + username=user, + password=location.key, + project_name=tenant_name, + user_domain_id=self.user_domain_id, + user_domain_name=self.user_domain_name, + project_domain_id=self.project_domain_id, + project_domain_name=self.project_domain_name) + sess = ks_session.Session(auth=password) + + return ks_client.Client(session=sess) + class MultiTenantStore(BaseStore): EXAMPLE_URL = "swift:////" @@ -992,6 +1043,73 @@ class MultiTenantStore(BaseStore): ssl_compression=self.ssl_compression, cacert=self.cacert) + def init_client(self, location, context=None): + # read client parameters from config files + ref_params = sutils.SwiftParams(self.conf).params + default_ref = self.conf.glance_store.default_swift_reference + default_swift_reference = ref_params.get(default_ref) + if not default_swift_reference: + reason = _("default_swift_reference %s is required.") % default_ref + LOG.error(reason) + raise exceptions.BadStoreConfiguration(message=reason) + + auth_address = default_swift_reference.get('auth_address') + user = default_swift_reference.get('user') + key = default_swift_reference.get('key') + user_domain_id = default_swift_reference.get('user_domain_id') + user_domain_name = default_swift_reference.get('user_domain_name') + project_domain_id = default_swift_reference.get('project_domain_id') + project_domain_name = default_swift_reference.get( + 'project_domain_name') + + # create client for multitenant user(trustor) + trustor_auth = ks_v3.Token(auth_url=auth_address, + token=context.auth_token, + project_id=context.tenant) + trustor_sess = ks_session.Session(auth=trustor_auth) + trustor_client = ks_client.Client(session=trustor_sess) + auth_ref = trustor_client.session.auth.get_auth_ref(trustor_sess) + roles = [t['name'] for t in auth_ref['roles']] + + # create client for trustee - glance user specified in swift config + tenant_name, user = user.split(':') + password = ks_v3.Password(auth_url=auth_address, + username=user, + password=key, + project_name=tenant_name, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name) + trustee_sess = ks_session.Session(auth=password) + trustee_client = ks_client.Client(session=trustee_sess) + + # request glance user id - we will use it as trustee user + trustee_user_id = trustee_client.session.get_user_id() + + # create trust for trustee user + trust_id = trustor_client.trusts.create( + trustee_user=trustee_user_id, trustor_user=context.user, + project=context.tenant, impersonation=True, + role_names=roles + ).id + # initialize a new client with trust and trustee credentials + # create client for glance trustee user + client_password = ks_v3.Password( + auth_url=auth_address, + username=user, + password=key, + trust_id=trust_id, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name + ) + # now we can authenticate against KS + # as trustee of user who provided token + client_sess = ks_session.Session(auth=client_password) + return ks_client.Client(session=client_sess) + class ChunkReader(object): def __init__(self, fd, checksum, total, verifier=None): diff --git a/glance_store/tests/unit/test_swift_store.py b/glance_store/tests/unit/test_swift_store.py index 4b0d0d1a..33abe0aa 100644 --- a/glance_store/tests/unit/test_swift_store.py +++ b/glance_store/tests/unit/test_swift_store.py @@ -22,6 +22,7 @@ import mock import tempfile import uuid +from keystoneclient import exceptions as ks_exceptions from oslo_config import cfg from oslo_utils import encodeutils from oslo_utils import units @@ -35,6 +36,7 @@ from six.moves import range import swiftclient from glance_store._drivers.swift import store as swift +from glance_store._drivers.swift import utils as sutils from glance_store import backend from glance_store import BackendException from glance_store import capabilities @@ -233,6 +235,12 @@ def stub_out_swiftclient(stubs, swift_store_auth_version): class SwiftTests(object): + def mock_keystone_client(self): + # mock keystone client functions to avoid dependency errors + swift.ks_v3 = mock.MagicMock() + swift.ks_session = mock.MagicMock() + swift.ks_client = mock.MagicMock() + @property def swift_store_user(self): return 'tenant:user1' @@ -285,10 +293,13 @@ class SwiftTests(object): resp_full = b''.join([chunk for chunk in image_swift.wrapped]) resp_half = resp_full[:len(resp_full) // 2] resp_half = six.BytesIO(resp_half) + manager = swift.get_manager_for_store(self.store, loc.store_location, + ctxt) + image_swift.wrapped = swift.swift_retry_iter(resp_half, image_size, self.store, loc.store_location, - ctxt) + manager) self.assertEqual(image_size, 5120) expected_data = b"*" * FIVE_KB @@ -337,6 +348,7 @@ class SwiftTests(object): def test_add(self): """Test that we can add an image via the swift backend.""" moves.reload_module(swift) + self.mock_keystone_client() self.store = Store(self.conf) self.store.configure() expected_swift_size = FIVE_KB @@ -374,6 +386,7 @@ class SwiftTests(object): conf['default_swift_reference'] = 'store_2' self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() self.store = Store(self.conf) self.store.configure() @@ -424,6 +437,7 @@ class SwiftTests(object): conf['default_swift_reference'] = variation self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() self.store = Store(self.conf) self.store.configure() loc, size, checksum, _ = self.store.add(image_id, image_swift, @@ -454,6 +468,7 @@ class SwiftTests(object): conf['swift_store_container'] = 'noexist' self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() self.store = Store(self.conf) self.store.configure() @@ -500,6 +515,7 @@ class SwiftTests(object): conf['swift_store_container'] = 'noexist' self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() self.store = Store(self.conf) self.store.configure() loc, size, checksum, _ = self.store.add(expected_image_id, @@ -545,6 +561,8 @@ class SwiftTests(object): conf['swift_store_multiple_containers_seed'] = 2 self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() + self.store = Store(self.conf) self.store.configure() loc, size, checksum, _ = self.store.add(expected_image_id, @@ -579,6 +597,7 @@ class SwiftTests(object): conf['swift_store_multiple_containers_seed'] = 2 self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() expected_image_id = str(uuid.uuid4()) expected_container = 'randomname_' + expected_image_id[:2] @@ -895,6 +914,7 @@ class SwiftTests(object): conf = copy.deepcopy(SWIFT_CONF) self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() self.store = Store(self.conf) self.store.configure() @@ -934,6 +954,7 @@ class SwiftTests(object): conf = copy.deepcopy(SWIFT_CONF) self.config(**conf) moves.reload_module(swift) + self.mock_keystone_client() self.store = Store(self.conf) self.store.configure() @@ -960,7 +981,7 @@ class SwiftTests(object): loc = location.get_location_from_uri(uri, conf=self.conf) self.store.delete(loc) - self.assertRaises(exceptions.NotFound, self.store.get, loc) + self.assertRaises(ks_exceptions.NotFound, self.store.get, loc) def test_delete_non_existing(self): """ @@ -1109,6 +1130,81 @@ class SwiftTests(object): self.assertRaises(NotImplementedError, swift.get_manager_for_store, store, loc) + @mock.patch("glance_store._drivers.swift.store.ks_v3") + @mock.patch("glance_store._drivers.swift.store.ks_session") + @mock.patch("glance_store._drivers.swift.store.ks_client") + def test_init_client_multi_tenant(self, + mock_client, mock_session, mock_v3): + """Test that keystone client was initialized correctly""" + # initialize store and connection parameters + self.config(swift_store_multi_tenant=True) + store = Store(self.conf) + store.configure() + ref_params = sutils.SwiftParams(self.conf).params + default_ref = self.conf.glance_store.default_swift_reference + default_swift_reference = ref_params.get(default_ref) + # prepare client and session + trustee_session = mock.MagicMock() + trustor_session = mock.MagicMock() + main_session = mock.MagicMock() + trustee_client = mock.MagicMock() + trustee_client.session.get_user_id.return_value = 'fake_user' + trustor_client = mock.MagicMock() + trustor_client.session.auth.get_auth_ref.return_value = { + 'roles': [{'name': 'fake_role'}] + } + trustor_client.trusts.create.return_value = mock.MagicMock( + id='fake_trust') + main_client = mock.MagicMock() + mock_session.Session.side_effect = [trustor_session, trustee_session, + main_session] + mock_client.Client.side_effect = [trustor_client, trustee_client, + main_client] + # initialize client + ctxt = mock.MagicMock() + client = store.init_client(location=mock.MagicMock(), context=ctxt) + # test trustor usage + mock_v3.Token.assert_called_once_with( + auth_url=default_swift_reference.get('auth_address'), + token=ctxt.auth_token, + project_id=ctxt.tenant + ) + mock_session.Session.assert_any_call(auth=mock_v3.Token()) + mock_client.Client.assert_any_call(session=trustor_session) + # test trustee usage and trust creation + tenant_name, user = default_swift_reference.get('user').split(':') + mock_v3.Password.assert_any_call( + auth_url=default_swift_reference.get('auth_address'), + username=user, + password=default_swift_reference.get('key'), + project_name=tenant_name, + user_domain_id=default_swift_reference.get('user_domain_id'), + user_domain_name=default_swift_reference.get('user_domain_name'), + project_domain_id=default_swift_reference.get('project_domain_id'), + project_domain_name=default_swift_reference.get( + 'project_domain_name') + ) + mock_session.Session.assert_any_call(auth=mock_v3.Password()) + mock_client.Client.assert_any_call(session=trustee_session) + trustor_client.trusts.create.assert_called_once_with( + trustee_user='fake_user', trustor_user=ctxt.user, + project=ctxt.tenant, impersonation=True, + role_names=['fake_role'] + ) + mock_v3.Password.assert_any_call( + auth_url=default_swift_reference.get('auth_address'), + username=user, + password=default_swift_reference.get('key'), + trust_id='fake_trust', + user_domain_id=default_swift_reference.get('user_domain_id'), + user_domain_name=default_swift_reference.get('user_domain_name'), + project_domain_id=default_swift_reference.get('project_domain_id'), + project_domain_name=default_swift_reference.get( + 'project_domain_name') + ) + mock_client.Client.assert_any_call(session=main_session) + self.assertEqual(main_client, client) + class TestStoreAuthV1(base.StoreBaseTest, SwiftTests, test_store_capabilities.TestStoreCapabilitiesChecking): @@ -1133,6 +1229,7 @@ class TestStoreAuthV1(base.StoreBaseTest, SwiftTests, moxfixture = self.useFixture(moxstubout.MoxStubout()) self.stubs = moxfixture.stubs stub_out_swiftclient(self.stubs, conf['swift_store_auth_version']) + self.mock_keystone_client() self.store = Store(self.conf) self.config(**conf) self.store.configure() @@ -1171,6 +1268,33 @@ class TestStoreAuthV3(TestStoreAuthV1): conf['swift_store_user'] = 'tenant:user1' return conf + @mock.patch("glance_store._drivers.swift.store.ks_v3") + @mock.patch("glance_store._drivers.swift.store.ks_session") + @mock.patch("glance_store._drivers.swift.store.ks_client") + def test_init_client_single_tenant(self, + mock_client, mock_session, mock_v3): + """Test that keystone client was initialized correctly""" + # initialize client + store = Store(self.conf) + store.configure() + uri = "swift://%s:key@auth_address/glance/%s" % ( + self.swift_store_user, FAKE_UUID) + loc = location.get_location_from_uri(uri, conf=self.conf) + ctxt = mock.MagicMock() + store.init_client(location=loc.store_location, context=ctxt) + # check that keystone was initialized correctly + tenant = None if store.auth_version == '1' else "tenant" + username = "tenant:user1" if store.auth_version == '1' else "user1" + mock_v3.Password.assert_called_once_with( + auth_url=loc.store_location.swift_url + '/', + username=username, password="key", + project_name=tenant, + project_domain_id=None, project_domain_name=None, + user_domain_id=None, user_domain_name=None,) + mock_session.Session.assert_called_once_with(auth=mock_v3.Password()) + mock_client.Client.assert_called_once_with( + session=mock_session.Session()) + class FakeConnection(object): def __init__(self, authurl=None, user=None, key=None, retries=5, diff --git a/releasenotes/notes/prevent-unauthorized-errors-ebb9cf2236595cd0.yaml b/releasenotes/notes/prevent-unauthorized-errors-ebb9cf2236595cd0.yaml new file mode 100644 index 00000000..7a1bae88 --- /dev/null +++ b/releasenotes/notes/prevent-unauthorized-errors-ebb9cf2236595cd0.yaml @@ -0,0 +1,12 @@ +--- +prelude: > + Prevent Unauthorized errors during uploading or + donwloading data to Swift store. +features: + - Allow glance_store to refresh token when upload or download data to Swift + store. glance_store identifies if token is going to expire soon when + executing request to Swift and refresh the token. For multi-tenant swift + store glance_store uses trusts, for single-tenant swift store glance_store + uses credentials from swift store configurations. Please also note that + this feature is enabled if and only if Keystone V3 API is available + and enabled. \ No newline at end of file