Implement trust support for api v2

Implement trust support for images api v2 when uploading images
with registry.
The algorithm is the following:
1. If 'registry' is set as data_api backend create a trust
2. Upload an image
3. Try to update the image status to 'active' in registry
4. If trust has been created succcessfully and 401 occured during
   updating the image status then renew the token and try to update
   the image record in registry again

Co-Authored-By: Mike Fedosin <mfedosin@mirantis.com>
Co-Authored-By: Kairat Kushaev <kkushaev@mirantis.com>

Implements bp trust-authentication

Change-Id: Ia3b82782b14f5dfc93457620633c1039c38fc366
This commit is contained in:
Mike Fedosin 2015-11-10 15:53:25 +03:00 committed by kairat_kushaev
parent 1efc49a933
commit 8df5c75446
5 changed files with 225 additions and 2 deletions

View File

@ -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)
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
View 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)

View File

@ -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)

View File

@ -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.

View File

@ -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