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:
parent
1efc49a933
commit
8df5c75446
@ -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