Merge "Implement trust support for api v2"
This commit is contained in:
commit
6971e57a45
@ -13,6 +13,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
import glance_store
|
import glance_store
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import encodeutils
|
from oslo_utils import encodeutils
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
@ -20,16 +21,19 @@ import webob.exc
|
|||||||
|
|
||||||
import glance.api.policy
|
import glance.api.policy
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
|
from glance.common import trust_auth
|
||||||
from glance.common import utils
|
from glance.common import utils
|
||||||
from glance.common import wsgi
|
from glance.common import wsgi
|
||||||
import glance.db
|
import glance.db
|
||||||
import glance.gateway
|
import glance.gateway
|
||||||
from glance.i18n import _, _LE
|
from glance.i18n import _, _LE, _LI
|
||||||
import glance.notifier
|
import glance.notifier
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
class ImageDataController(object):
|
class ImageDataController(object):
|
||||||
def __init__(self, db_api=None, store_api=None,
|
def __init__(self, db_api=None, store_api=None,
|
||||||
@ -81,13 +85,54 @@ class ImageDataController(object):
|
|||||||
def upload(self, req, image_id, data, size):
|
def upload(self, req, image_id, data, size):
|
||||||
image_repo = self.gateway.get_repo(req.context)
|
image_repo = self.gateway.get_repo(req.context)
|
||||||
image = None
|
image = None
|
||||||
|
refresher = None
|
||||||
|
cxt = req.context
|
||||||
try:
|
try:
|
||||||
image = image_repo.get(image_id)
|
image = image_repo.get(image_id)
|
||||||
image.status = 'saving'
|
image.status = 'saving'
|
||||||
try:
|
try:
|
||||||
|
if CONF.data_api == 'glance.db.registry.api':
|
||||||
|
# create a trust if backend is registry
|
||||||
|
try:
|
||||||
|
# request user pluging for current token
|
||||||
|
user_plugin = req.environ.get('keystone.token_auth')
|
||||||
|
roles = []
|
||||||
|
# use roles from request environment because they
|
||||||
|
# are not transformed to lower-case unlike cxt.roles
|
||||||
|
for role_info in req.environ.get(
|
||||||
|
'keystone.token_info')['token']['roles']:
|
||||||
|
roles.append(role_info['name'])
|
||||||
|
refresher = trust_auth.TokenRefresher(user_plugin,
|
||||||
|
cxt.tenant,
|
||||||
|
roles)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info(_LI("Unable to create trust: %s "
|
||||||
|
"Use the existing user token."),
|
||||||
|
encodeutils.exception_to_unicode(e))
|
||||||
|
|
||||||
image_repo.save(image)
|
image_repo.save(image)
|
||||||
image.set_data(data, size)
|
image.set_data(data, size)
|
||||||
|
|
||||||
|
try:
|
||||||
image_repo.save(image, from_state='saving')
|
image_repo.save(image, from_state='saving')
|
||||||
|
except exception.NotAuthenticated as e:
|
||||||
|
if refresher is not None:
|
||||||
|
# request a new token to update an image in database
|
||||||
|
cxt.auth_token = refresher.refresh_token()
|
||||||
|
image_repo = self.gateway.get_repo(req.context)
|
||||||
|
image_repo.save(image, from_state='saving')
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
try:
|
||||||
|
# release resources required for re-auth
|
||||||
|
if refresher is not None:
|
||||||
|
refresher.release_resources()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info(_LI("Unable to delete trust %(trust)s: %(msg)s"),
|
||||||
|
{"trust": refresher.trust_id,
|
||||||
|
"msg": encodeutils.exception_to_unicode(e)})
|
||||||
|
|
||||||
except (glance_store.NotFound,
|
except (glance_store.NotFound,
|
||||||
exception.ImageNotFound,
|
exception.ImageNotFound,
|
||||||
exception.Conflict):
|
exception.Conflict):
|
||||||
|
117
glance/common/trust_auth.py
Normal file
117
glance/common/trust_auth.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# 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 import v3
|
||||||
|
from keystoneauth1.loading import conf
|
||||||
|
from keystoneauth1.loading import session
|
||||||
|
from keystoneclient import exceptions as ks_exceptions
|
||||||
|
from keystoneclient.v3 import client as ks_client
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TokenRefresher(object):
|
||||||
|
"""Class that responsible for token refreshing with trusts"""
|
||||||
|
|
||||||
|
def __init__(self, user_plugin, user_project, user_roles):
|
||||||
|
"""Prepare all parameters and clients required to refresh token"""
|
||||||
|
|
||||||
|
# step 1: Prepare parameters required to connect to keystone
|
||||||
|
self.auth_url = CONF.keystone_authtoken.auth_uri
|
||||||
|
if not self.auth_url.endswith('/v3'):
|
||||||
|
self.auth_url += '/v3'
|
||||||
|
|
||||||
|
self.ssl_settings = {
|
||||||
|
'cacert': CONF.keystone_authtoken.cafile,
|
||||||
|
'insecure': CONF.keystone_authtoken.insecure,
|
||||||
|
'cert': CONF.keystone_authtoken.certfile,
|
||||||
|
'key': CONF.keystone_authtoken.keyfile,
|
||||||
|
}
|
||||||
|
|
||||||
|
# step 2: create trust to ensure that we can always update token
|
||||||
|
|
||||||
|
# trustor = user who made the request
|
||||||
|
trustor_client = self._load_client(user_plugin, self.ssl_settings)
|
||||||
|
trustor_id = trustor_client.session.get_user_id()
|
||||||
|
|
||||||
|
# get trustee user client that impersonates main user
|
||||||
|
trustee_user_auth = conf.load_from_conf_options(CONF,
|
||||||
|
'keystone_authtoken')
|
||||||
|
# save service user client because we need new service token
|
||||||
|
# to refresh trust-scoped client later
|
||||||
|
self.trustee_user_client = self._load_client(trustee_user_auth,
|
||||||
|
self.ssl_settings)
|
||||||
|
trustee_id = self.trustee_user_client.session.get_user_id()
|
||||||
|
|
||||||
|
self.trust_id = trustor_client.trusts.create(trustor_user=trustor_id,
|
||||||
|
trustee_user=trustee_id,
|
||||||
|
impersonation=True,
|
||||||
|
role_names=user_roles,
|
||||||
|
project=user_project).id
|
||||||
|
LOG.debug("Trust %s has been created.", self.trust_id)
|
||||||
|
|
||||||
|
# step 3: postpone trust-scoped client initialization
|
||||||
|
# until we need to refresh the token
|
||||||
|
self.trustee_client = None
|
||||||
|
|
||||||
|
def refresh_token(self):
|
||||||
|
"""Receive new token if user need to update old token
|
||||||
|
|
||||||
|
:return: new token that can be used for authentication
|
||||||
|
"""
|
||||||
|
LOG.debug("Requesting the new token with trust %s", self.trust_id)
|
||||||
|
if self.trustee_client is None:
|
||||||
|
self.trustee_client = self._refresh_trustee_client()
|
||||||
|
try:
|
||||||
|
return self.trustee_client.session.get_token()
|
||||||
|
except ks_exceptions.Unauthorized:
|
||||||
|
# in case of Unauthorized exceptions try to refresh client because
|
||||||
|
# service user token may expired
|
||||||
|
self.trustee_client = self._refresh_trustee_client()
|
||||||
|
return self.trustee_client.session.get_token()
|
||||||
|
|
||||||
|
def release_resources(self):
|
||||||
|
"""Release keystone resources required for refreshing"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.trustee_client is None:
|
||||||
|
self._refresh_trustee_client().trusts.delete(self.trust_id)
|
||||||
|
else:
|
||||||
|
self.trustee_client.trusts.delete(self.trust_id)
|
||||||
|
except ks_exceptions.Unauthorized:
|
||||||
|
# service user token may expire when we are trying to delete token
|
||||||
|
# so need to update client to ensure that this is not the reason
|
||||||
|
# of failure
|
||||||
|
self.trustee_client = self._refresh_trustee_client()
|
||||||
|
self.trustee_client.trusts.delete(self.trust_id)
|
||||||
|
|
||||||
|
def _refresh_trustee_client(self):
|
||||||
|
trustee_token = self.trustee_user_client.session.get_token()
|
||||||
|
trustee_auth = v3.Token(
|
||||||
|
trust_id=self.trust_id,
|
||||||
|
token=trustee_token,
|
||||||
|
auth_url=self.auth_url
|
||||||
|
)
|
||||||
|
return self._load_client(trustee_auth, self.ssl_settings)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_client(plugin, ssl_settings):
|
||||||
|
# load client from auth settings and user plugin
|
||||||
|
sess = session.Session().load_from_options(
|
||||||
|
auth=plugin, **ssl_settings)
|
||||||
|
return ks_client.Client(session=sess)
|
@ -365,6 +365,59 @@ class TestImagesController(base.StoreClearingUnitTest):
|
|||||||
self.controller.upload,
|
self.controller.upload,
|
||||||
request, unit_test_utils.UUID2, 'YY', 2)
|
request, unit_test_utils.UUID2, 'YY', 2)
|
||||||
|
|
||||||
|
@mock.patch("glance.common.trust_auth.TokenRefresher")
|
||||||
|
def test_upload_with_trusts(self, mock_refresher):
|
||||||
|
"""Test that uploading with registry correctly uses trusts"""
|
||||||
|
# initialize trust environment
|
||||||
|
self.config(data_api='glance.db.registry.api')
|
||||||
|
refresher = mock.MagicMock()
|
||||||
|
mock_refresher.return_value = refresher
|
||||||
|
refresher.refresh_token.return_value = "fake_token"
|
||||||
|
# request an image upload
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
request.environ['keystone.token_auth'] = mock.MagicMock()
|
||||||
|
request.environ['keystone.token_info'] = {
|
||||||
|
'token': {
|
||||||
|
'roles': [{'name': 'FakeRole', 'id': 'FakeID'}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
image = FakeImage('abcd')
|
||||||
|
self.image_repo.result = image
|
||||||
|
mock_fake_save = mock.Mock()
|
||||||
|
mock_fake_save.side_effect = [None, exception.NotAuthenticated, None]
|
||||||
|
temp_save = FakeImageRepo.save
|
||||||
|
# mocking save to raise NotAuthenticated on the second call
|
||||||
|
FakeImageRepo.save = mock_fake_save
|
||||||
|
self.controller.upload(request, unit_test_utils.UUID2, 'YYYY', 4)
|
||||||
|
# check image data
|
||||||
|
self.assertEqual('YYYY', image.data)
|
||||||
|
self.assertEqual(4, image.size)
|
||||||
|
FakeImageRepo.save = temp_save
|
||||||
|
# check that token has been correctly acquired and deleted
|
||||||
|
mock_refresher.assert_called_once_with(
|
||||||
|
request.environ['keystone.token_auth'],
|
||||||
|
request.context.tenant, ['FakeRole'])
|
||||||
|
refresher.refresh_token.assert_called_once_with()
|
||||||
|
refresher.release_resources.assert_called_once_with()
|
||||||
|
self.assertEqual("fake_token", request.context.auth_token)
|
||||||
|
|
||||||
|
@mock.patch("glance.common.trust_auth.TokenRefresher")
|
||||||
|
def test_upload_with_trusts_fails(self, mock_refresher):
|
||||||
|
"""Test upload with registry if trust was not successfully created"""
|
||||||
|
# initialize trust environment
|
||||||
|
self.config(data_api='glance.db.registry.api')
|
||||||
|
mock_refresher().side_effect = Exception()
|
||||||
|
# request an image upload
|
||||||
|
request = unit_test_utils.get_fake_request()
|
||||||
|
image = FakeImage('abcd')
|
||||||
|
self.image_repo.result = image
|
||||||
|
self.controller.upload(request, unit_test_utils.UUID2, 'YYYY', 4)
|
||||||
|
# check image data
|
||||||
|
self.assertEqual('YYYY', image.data)
|
||||||
|
self.assertEqual(4, image.size)
|
||||||
|
# check that the token has not been updated
|
||||||
|
self.assertEqual(0, mock_refresher().refresh_token.call_count)
|
||||||
|
|
||||||
def _test_upload_download_prepare_notification(self):
|
def _test_upload_download_prepare_notification(self):
|
||||||
request = unit_test_utils.get_fake_request()
|
request = unit_test_utils.get_fake_request()
|
||||||
self.controller.upload(request, unit_test_utils.UUID2, 'YYYY', 4)
|
self.controller.upload(request, unit_test_utils.UUID2, 'YYYY', 4)
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Implemented re-authentication with trusts when updating image status in
|
||||||
|
registry after image upload. When long-running image upload takes some a lot
|
||||||
|
of time (more than token expiration time) glance uses trusts to receive new
|
||||||
|
token and update image status in registry. It allows users to upload big
|
||||||
|
size images without increasing token expiration time.
|
@ -22,6 +22,7 @@ oslo.utils>=3.2.0 # Apache-2.0
|
|||||||
stevedore>=1.5.0 # Apache-2.0
|
stevedore>=1.5.0 # Apache-2.0
|
||||||
futurist>=0.6.0 # Apache-2.0
|
futurist>=0.6.0 # Apache-2.0
|
||||||
taskflow>=1.25.0
|
taskflow>=1.25.0
|
||||||
|
keystoneauth1>=2.1.0
|
||||||
keystonemiddleware>=4.0.0
|
keystonemiddleware>=4.0.0
|
||||||
WSME>=0.8
|
WSME>=0.8
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user