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
|
||||
# under the License.
|
||||
import glance_store
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import excutils
|
||||
@ -20,16 +21,19 @@ import webob.exc
|
||||
|
||||
import glance.api.policy
|
||||
from glance.common import exception
|
||||
from glance.common import trust_auth
|
||||
from glance.common import utils
|
||||
from glance.common import wsgi
|
||||
import glance.db
|
||||
import glance.gateway
|
||||
from glance.i18n import _, _LE
|
||||
from glance.i18n import _, _LE, _LI
|
||||
import glance.notifier
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class ImageDataController(object):
|
||||
def __init__(self, db_api=None, store_api=None,
|
||||
@ -81,13 +85,54 @@ class ImageDataController(object):
|
||||
def upload(self, req, image_id, data, size):
|
||||
image_repo = self.gateway.get_repo(req.context)
|
||||
image = None
|
||||
refresher = None
|
||||
cxt = req.context
|
||||
try:
|
||||
image = image_repo.get(image_id)
|
||||
image.status = 'saving'
|
||||
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.set_data(data, size)
|
||||
image_repo.save(image, from_state='saving')
|
||||
|
||||
try:
|
||||
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,
|
||||
exception.ImageNotFound,
|
||||
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,
|
||||
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):
|
||||
request = unit_test_utils.get_fake_request()
|
||||
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
|
||||
futurist>=0.6.0 # Apache-2.0
|
||||
taskflow>=1.25.0
|
||||
keystoneauth1>=2.1.0
|
||||
keystonemiddleware>=4.0.0
|
||||
WSME>=0.8
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user