summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2016-03-01 09:35:09 +0000
committerGerrit Code Review <review@openstack.org>2016-03-01 09:35:09 +0000
commit6c4ae678f57390ef845c446edbcce9a6f93e1927 (patch)
tree25cb9ef253561904e268f4581f9ebbe076218518
parent14eac131845bf2762668d665d3283b5abd87eeb2 (diff)
parent1b782cee8552ec02f7303ee6f9ba9d1f2c180d07 (diff)
downloadglance_store-0.12.0.tar.gz
Merge "Implement re-authentication for swift driver"0.12.0
-rw-r--r--glance_store/_drivers/swift/store.py416
-rw-r--r--glance_store/tests/unit/test_swift_store.py128
-rw-r--r--releasenotes/notes/prevent-unauthorized-errors-ebb9cf2236595cd0.yaml12
3 files changed, 405 insertions, 151 deletions
diff --git a/glance_store/_drivers/swift/store.py b/glance_store/_drivers/swift/store.py
index ef3fcd4..bf42d05 100644
--- a/glance_store/_drivers/swift/store.py
+++ b/glance_store/_drivers/swift/store.py
@@ -33,6 +33,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
@@ -140,7 +144,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
@@ -185,9 +189,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
@@ -440,16 +444,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:
@@ -466,21 +468,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)
-
- 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)
+ # 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 ''
+
+ 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
@@ -516,138 +523,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))))
+ 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:
+ obj_etag = manager.get_connection().put_object(
+ location.container, location.obj,
+ image_file, content_length=image_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
+ # 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:
- 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
- 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)
-
- 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
-
- # 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
- 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/<ID> and
- # GET /images/details
- if sutils.is_multiple_swift_store_accounts_enabled(self.conf):
- include_creds = False
- else:
- include_creds = True
+ # 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 = '?'
- 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)
+ 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
+ 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)
+
+ 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
+
+ # 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}
+
+ # 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()
+
+ # 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/<ID> 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):
@@ -766,7 +778,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):
@@ -905,6 +923,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://<SWIFT_URL>/<CONTAINER>/<FILE>"
@@ -1000,6 +1051,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 40d1a5a..cf1e3d3 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 0000000..7a1bae8
--- /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