s3 secret caching
To increase performance of the s3 API retrieve and cache s3 secret from keystone to allow for local validation. Disabled by default, to use set 'secret_cache_duration' to a number greater than 0. You will also need to configure keystone auth credentials in the s3token configuration group too. These credentials will need to be able to view all projects credentials in keystone. Change-Id: Id0c01da6aa6ca804c8f49a307b5171b87ec92228
This commit is contained in:
@@ -6,3 +6,4 @@ sphinx>=1.6.2 # BSD
|
|||||||
openstackdocstheme>=1.11.0 # Apache-2.0
|
openstackdocstheme>=1.11.0 # Apache-2.0
|
||||||
reno>=1.8.0 # Apache-2.0
|
reno>=1.8.0 # Apache-2.0
|
||||||
os-api-ref>=1.0.0 # Apache-2.0
|
os-api-ref>=1.0.0 # Apache-2.0
|
||||||
|
python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0
|
||||||
|
|||||||
@@ -30,19 +30,25 @@ This middleware:
|
|||||||
access key.
|
access key.
|
||||||
* Validates s3 token with Keystone.
|
* Validates s3 token with Keystone.
|
||||||
* Transforms the account name to AUTH_%(tenant_name).
|
* Transforms the account name to AUTH_%(tenant_name).
|
||||||
|
* Optionally can retrieve and cache secret from keystone
|
||||||
|
to validate signature locally
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from keystoneclient.v3 import client as keystone_client
|
||||||
|
from keystoneauth1 import session as keystone_session
|
||||||
|
from keystoneauth1 import loading as keystone_loading
|
||||||
import requests
|
import requests
|
||||||
import six
|
import six
|
||||||
from six.moves import urllib
|
from six.moves import urllib
|
||||||
|
|
||||||
from swift.common.swob import Request, HTTPBadRequest, HTTPUnauthorized, \
|
from swift.common.swob import Request, HTTPBadRequest, HTTPUnauthorized, \
|
||||||
HTTPException
|
HTTPException
|
||||||
from swift.common.utils import config_true_value, split_path, get_logger
|
from swift.common.utils import config_true_value, split_path, get_logger, \
|
||||||
|
cache_from_env
|
||||||
from swift.common.wsgi import ConfigFileError
|
from swift.common.wsgi import ConfigFileError
|
||||||
|
|
||||||
|
|
||||||
@@ -155,6 +161,31 @@ class S3Token(object):
|
|||||||
else:
|
else:
|
||||||
self._verify = None
|
self._verify = None
|
||||||
|
|
||||||
|
self._secret_cache_duration = int(conf.get('secret_cache_duration', 0))
|
||||||
|
if self._secret_cache_duration > 0:
|
||||||
|
try:
|
||||||
|
auth_plugin = keystone_loading.get_plugin_loader(
|
||||||
|
conf.get('auth_type'))
|
||||||
|
available_auth_options = auth_plugin.get_options()
|
||||||
|
auth_options = {}
|
||||||
|
for option in available_auth_options:
|
||||||
|
name = option.name.replace('-', '_')
|
||||||
|
value = conf.get(name)
|
||||||
|
if value:
|
||||||
|
auth_options[name] = value
|
||||||
|
|
||||||
|
auth = auth_plugin.load_from_options(**auth_options)
|
||||||
|
session = keystone_session.Session(auth=auth)
|
||||||
|
self.keystoneclient = keystone_client.Client(session=session)
|
||||||
|
self._logger.info("Caching s3tokens for %s seconds",
|
||||||
|
self._secret_cache_duration)
|
||||||
|
except Exception:
|
||||||
|
self._logger.warning("Unable to load keystone auth_plugin. "
|
||||||
|
"Secret caching will be unavailable.",
|
||||||
|
exc_info=True)
|
||||||
|
self.keystoneclient = None
|
||||||
|
self._secret_cache_duration = 0
|
||||||
|
|
||||||
def _deny_request(self, code):
|
def _deny_request(self, code):
|
||||||
error_cls, message = {
|
error_cls, message = {
|
||||||
'AccessDenied': (HTTPUnauthorized, 'Access denied'),
|
'AccessDenied': (HTTPUnauthorized, 'Access denied'),
|
||||||
@@ -245,6 +276,24 @@ class S3Token(object):
|
|||||||
creds = {'credentials': {'access': access,
|
creds = {'credentials': {'access': access,
|
||||||
'token': token,
|
'token': token,
|
||||||
'signature': signature}}
|
'signature': signature}}
|
||||||
|
|
||||||
|
memcache_client = None
|
||||||
|
memcache_token_key = 's3secret/%s' % access
|
||||||
|
if self._secret_cache_duration > 0:
|
||||||
|
memcache_client = cache_from_env(environ)
|
||||||
|
cached_auth_data = None
|
||||||
|
|
||||||
|
if memcache_client:
|
||||||
|
cached_auth_data = memcache_client.get(memcache_token_key)
|
||||||
|
if cached_auth_data:
|
||||||
|
headers, token_id, tenant, secret = cached_auth_data
|
||||||
|
if s3_auth_details['check_signature'](secret):
|
||||||
|
self._logger.debug("Cached creds valid")
|
||||||
|
else:
|
||||||
|
self._logger.debug("Cached creds invalid")
|
||||||
|
cached_auth_data = None
|
||||||
|
|
||||||
|
if not cached_auth_data:
|
||||||
creds_json = json.dumps(creds)
|
creds_json = json.dumps(creds)
|
||||||
self._logger.debug('Connecting to Keystone sending this JSON: %s',
|
self._logger.debug('Connecting to Keystone sending this JSON: %s',
|
||||||
creds_json)
|
creds_json)
|
||||||
@@ -261,7 +310,8 @@ class S3Token(object):
|
|||||||
resp = self._json_request(creds_json)
|
resp = self._json_request(creds_json)
|
||||||
except HTTPException as e_resp:
|
except HTTPException as e_resp:
|
||||||
if self._delay_auth_decision:
|
if self._delay_auth_decision:
|
||||||
msg = 'Received error, deferring rejection based on error: %s'
|
msg = ('Received error, deferring rejection based on '
|
||||||
|
'error: %s')
|
||||||
self._logger.debug(msg, e_resp.status)
|
self._logger.debug(msg, e_resp.status)
|
||||||
return self._app(environ, start_response)
|
return self._app(environ, start_response)
|
||||||
else:
|
else:
|
||||||
@@ -281,6 +331,22 @@ class S3Token(object):
|
|||||||
headers, token_id, tenant = parse_v3_response(token)
|
headers, token_id, tenant = parse_v3_response(token)
|
||||||
else:
|
else:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
if memcache_client:
|
||||||
|
user_id = headers.get('X-User-Id')
|
||||||
|
if not user_id:
|
||||||
|
raise ValueError
|
||||||
|
try:
|
||||||
|
cred_ref = self.keystoneclient.ec2.get(
|
||||||
|
user_id=user_id,
|
||||||
|
access=access)
|
||||||
|
memcache_client.set(
|
||||||
|
memcache_token_key,
|
||||||
|
(headers, token_id, tenant, cred_ref.secret),
|
||||||
|
time=self._secret_cache_duration)
|
||||||
|
self._logger.debug("Cached keystone credentials")
|
||||||
|
except Exception:
|
||||||
|
self._logger.warning("Unable to cache secret",
|
||||||
|
exc_info=True)
|
||||||
|
|
||||||
# Populate the environment similar to auth_token,
|
# Populate the environment similar to auth_token,
|
||||||
# so we don't have to contact Keystone again.
|
# so we don't have to contact Keystone again.
|
||||||
@@ -288,7 +354,6 @@ class S3Token(object):
|
|||||||
# Note that although the strings are unicode following json
|
# Note that although the strings are unicode following json
|
||||||
# deserialization, Swift's HeaderEnvironProxy handles ensuring
|
# deserialization, Swift's HeaderEnvironProxy handles ensuring
|
||||||
# they're stored as native strings
|
# they're stored as native strings
|
||||||
req.headers.update(headers)
|
|
||||||
req.environ['keystone.token_info'] = token
|
req.environ['keystone.token_info'] = token
|
||||||
except (ValueError, KeyError, TypeError):
|
except (ValueError, KeyError, TypeError):
|
||||||
if self._delay_auth_decision:
|
if self._delay_auth_decision:
|
||||||
@@ -303,6 +368,7 @@ class S3Token(object):
|
|||||||
return self._deny_request('InvalidURI')(
|
return self._deny_request('InvalidURI')(
|
||||||
environ, start_response)
|
environ, start_response)
|
||||||
|
|
||||||
|
req.headers.update(headers)
|
||||||
req.headers['X-Auth-Token'] = token_id
|
req.headers['X-Auth-Token'] = token_id
|
||||||
tenant_to_connect = force_tenant or tenant['id']
|
tenant_to_connect = force_tenant or tenant['id']
|
||||||
if six.PY2 and isinstance(tenant_to_connect, six.text_type):
|
if six.PY2 and isinstance(tenant_to_connect, six.text_type):
|
||||||
|
|||||||
@@ -504,6 +504,98 @@ class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase):
|
|||||||
self._assert_authorized(req, account_path='/v1/')
|
self._assert_authorized(req, account_path='/v1/')
|
||||||
self.assertEqual(req.environ['PATH_INFO'], '/v1/AUTH_TENANT_ID/c/o')
|
self.assertEqual(req.environ['PATH_INFO'], '/v1/AUTH_TENANT_ID/c/o')
|
||||||
|
|
||||||
|
@mock.patch('swift.common.middleware.s3api.s3token.cache_from_env')
|
||||||
|
@mock.patch('keystoneclient.v3.client.Client')
|
||||||
|
@mock.patch.object(requests, 'post')
|
||||||
|
def test_secret_is_cached(self, MOCK_REQUEST, MOCK_KEYSTONE,
|
||||||
|
MOCK_CACHE_FROM_ENV):
|
||||||
|
self.middleware = s3token.filter_factory({
|
||||||
|
'auth_uri': 'http://example.com',
|
||||||
|
'secret_cache_duration': '20',
|
||||||
|
'auth_type': 'v3password',
|
||||||
|
'auth_url': 'http://example.com:5000/v3',
|
||||||
|
'username': 'swift',
|
||||||
|
'password': 'secret',
|
||||||
|
'project_name': 'service',
|
||||||
|
'user_domain_name': 'default',
|
||||||
|
'project_domain_name': 'default',
|
||||||
|
})(FakeApp())
|
||||||
|
self.assertEqual(20, self.middleware._secret_cache_duration)
|
||||||
|
|
||||||
|
cache = MOCK_CACHE_FROM_ENV.return_value
|
||||||
|
|
||||||
|
fake_cache_response = ({}, 'token_id', {'id': 'tenant_id'}, 'secret')
|
||||||
|
cache.get.return_value = fake_cache_response
|
||||||
|
|
||||||
|
MOCK_REQUEST.return_value = TestResponse({
|
||||||
|
'status_code': 201,
|
||||||
|
'text': json.dumps(GOOD_RESPONSE_V2)})
|
||||||
|
|
||||||
|
req = Request.blank('/v1/AUTH_cfa/c/o')
|
||||||
|
req.environ['s3api.auth_details'] = {
|
||||||
|
'access_key': u'access',
|
||||||
|
'signature': u'signature',
|
||||||
|
'string_to_sign': u'token',
|
||||||
|
'check_signature': lambda x: True
|
||||||
|
}
|
||||||
|
req.get_response(self.middleware)
|
||||||
|
# Ensure we don't request auth from keystone
|
||||||
|
self.assertFalse(MOCK_REQUEST.called)
|
||||||
|
|
||||||
|
@mock.patch('swift.common.middleware.s3api.s3token.cache_from_env')
|
||||||
|
@mock.patch('keystoneclient.v3.client.Client')
|
||||||
|
@mock.patch.object(requests, 'post')
|
||||||
|
def test_secret_sets_cache(self, MOCK_REQUEST, MOCK_KEYSTONE,
|
||||||
|
MOCK_CACHE_FROM_ENV):
|
||||||
|
self.middleware = s3token.filter_factory({
|
||||||
|
'auth_uri': 'http://example.com',
|
||||||
|
'secret_cache_duration': '20',
|
||||||
|
'auth_type': 'v3password',
|
||||||
|
'auth_url': 'http://example.com:5000/v3',
|
||||||
|
'username': 'swift',
|
||||||
|
'password': 'secret',
|
||||||
|
'project_name': 'service',
|
||||||
|
'user_domain_name': 'default',
|
||||||
|
'project_domain_name': 'default',
|
||||||
|
})(FakeApp())
|
||||||
|
self.assertEqual(20, self.middleware._secret_cache_duration)
|
||||||
|
|
||||||
|
cache = MOCK_CACHE_FROM_ENV.return_value
|
||||||
|
cache.get.return_value = None
|
||||||
|
|
||||||
|
keystone_client = MOCK_KEYSTONE.return_value
|
||||||
|
keystone_client.ec2.get.return_value = mock.Mock(secret='secret')
|
||||||
|
|
||||||
|
MOCK_REQUEST.return_value = TestResponse({
|
||||||
|
'status_code': 201,
|
||||||
|
'text': json.dumps(GOOD_RESPONSE_V2)})
|
||||||
|
|
||||||
|
req = Request.blank('/v1/AUTH_cfa/c/o')
|
||||||
|
req.environ['s3api.auth_details'] = {
|
||||||
|
'access_key': u'access',
|
||||||
|
'signature': u'signature',
|
||||||
|
'string_to_sign': u'token',
|
||||||
|
'check_signature': lambda x: True
|
||||||
|
}
|
||||||
|
req.get_response(self.middleware)
|
||||||
|
expected_headers = {
|
||||||
|
'X-Identity-Status': u'Confirmed',
|
||||||
|
'X-Roles': u'swift-user,_member_',
|
||||||
|
'X-User-Id': u'USER_ID',
|
||||||
|
'X-User-Name': u'S3_USER',
|
||||||
|
'X-Tenant-Id': u'TENANT_ID',
|
||||||
|
'X-Tenant-Name': u'TENANT_NAME',
|
||||||
|
'X-Project-Id': u'TENANT_ID',
|
||||||
|
'X-Project-Name': u'TENANT_NAME',
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertTrue(MOCK_REQUEST.called)
|
||||||
|
tenant = GOOD_RESPONSE_V2['access']['token']['tenant']
|
||||||
|
token = GOOD_RESPONSE_V2['access']['token']['id']
|
||||||
|
expected_cache = (expected_headers, token, tenant, 'secret')
|
||||||
|
cache.set.assert_called_once_with('s3secret/access', expected_cache,
|
||||||
|
time=20)
|
||||||
|
|
||||||
|
|
||||||
class S3TokenMiddlewareTestBad(S3TokenMiddlewareTestBase):
|
class S3TokenMiddlewareTestBad(S3TokenMiddlewareTestBase):
|
||||||
def test_unauthorized_token(self):
|
def test_unauthorized_token(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user