Files
swift/test/unit/common/middleware/s3api/test_s3token.py
Sam Morrison b0aea93603 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
2018-12-11 18:41:36 +00:00

958 lines
36 KiB
Python

# Copyright 2012 OpenStack Foundation
#
# 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.
import copy
import base64
import json
import logging
import time
import unittest
import uuid
import fixtures
import mock
import requests
from requests_mock.contrib import fixture as rm_fixture
from six.moves import urllib
from swift.common.middleware.s3api import s3token
from swift.common.swob import Request, Response
from swift.common.wsgi import ConfigFileError
GOOD_RESPONSE_V2 = {'access': {
'user': {
'username': 'S3_USER',
'name': 'S3_USER',
'id': 'USER_ID',
'roles': [
{'name': 'swift-user'},
{'name': '_member_'},
],
},
'token': {
'id': 'TOKEN_ID',
'tenant': {
'id': 'TENANT_ID',
'name': 'TENANT_NAME'
}
}
}}
GOOD_RESPONSE_V3 = {'token': {
'user': {
'domain': {
'name': 'Default',
'id': 'default',
},
'name': 'S3_USER',
'id': 'USER_ID',
},
'project': {
'domain': {
'name': 'PROJECT_DOMAIN_NAME',
'id': 'PROJECT_DOMAIN_ID',
},
'name': 'PROJECT_NAME',
'id': 'PROJECT_ID',
},
'roles': [
{'name': 'swift-user'},
{'name': '_member_'},
],
}}
class TestResponse(requests.Response):
"""Utility class to wrap requests.Response.
Class used to wrap requests.Response and provide some convenience to
initialize with a dict.
"""
def __init__(self, data):
self._text = None
super(TestResponse, self).__init__()
if isinstance(data, dict):
self.status_code = data.get('status_code', 200)
headers = data.get('headers')
if headers:
self.headers.update(headers)
# Fake the text attribute to streamline Response creation
# _content is defined by requests.Response
self._content = data.get('text')
else:
self.status_code = data
def __eq__(self, other):
return self.__dict__ == other.__dict__
@property
def text(self):
return self.content
class FakeApp(object):
calls = 0
"""This represents a WSGI app protected by the auth_token middleware."""
def __call__(self, env, start_response):
self.calls += 1
resp = Response()
resp.environ = env
return resp(env, start_response)
class S3TokenMiddlewareTestBase(unittest.TestCase):
TEST_AUTH_URI = 'https://fakehost/identity/v2.0'
TEST_URL = '%s/s3tokens' % (TEST_AUTH_URI, )
TEST_DOMAIN_ID = '1'
TEST_DOMAIN_NAME = 'aDomain'
TEST_GROUP_ID = uuid.uuid4().hex
TEST_ROLE_ID = uuid.uuid4().hex
TEST_TENANT_ID = '1'
TEST_TENANT_NAME = 'aTenant'
TEST_TOKEN = 'aToken'
TEST_TRUST_ID = 'aTrust'
TEST_USER = 'test'
TEST_USER_ID = uuid.uuid4().hex
TEST_ROOT_URL = 'http://127.0.0.1:5000/'
def setUp(self):
super(S3TokenMiddlewareTestBase, self).setUp()
self.logger = fixtures.FakeLogger(level=logging.DEBUG)
self.logger.setUp()
self.time_patcher = mock.patch.object(time, 'time', lambda: 1234)
self.time_patcher.start()
self.app = FakeApp()
self.conf = {
'auth_uri': self.TEST_AUTH_URI,
}
self.middleware = s3token.S3Token(self.app, self.conf)
self.requests_mock = rm_fixture.Fixture()
self.requests_mock.setUp()
def tearDown(self):
self.requests_mock.cleanUp()
self.time_patcher.stop()
self.logger.cleanUp()
super(S3TokenMiddlewareTestBase, self).tearDown()
def start_fake_response(self, status, headers):
self.response_status = int(status.split(' ', 1)[0])
self.response_headers = dict(headers)
class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase):
def setUp(self):
super(S3TokenMiddlewareTestGood, self).setUp()
self.requests_mock.post(self.TEST_URL,
status_code=201,
json=GOOD_RESPONSE_V2)
# Ignore the request and pass to the next middleware in the
# pipeline if no path has been specified.
def test_no_path_request(self):
req = Request.blank('/')
self.middleware(req.environ, self.start_fake_response)
self.assertEqual(self.response_status, 200)
# Ignore the request and pass to the next middleware in the
# pipeline if no Authorization header has been specified
def test_without_authorization(self):
req = Request.blank('/v1/AUTH_cfa/c/o')
self.middleware(req.environ, self.start_fake_response)
self.assertEqual(self.response_status, 200)
def test_nukes_auth_headers(self):
client_env = {
'HTTP_X_IDENTITY_STATUS': 'Confirmed',
'HTTP_X_ROLES': 'admin,_member_,swift-user',
'HTTP_X_TENANT_ID': 'cfa'
}
req = Request.blank('/v1/AUTH_cfa/c/o', environ=client_env)
self.middleware(req.environ, self.start_fake_response)
self.assertEqual(self.response_status, 200)
for key in client_env:
self.assertNotIn(key, req.environ)
def test_without_auth_storage_token(self):
req = Request.blank('/v1/AUTH_cfa/c/o')
req.headers['Authorization'] = 'AWS badboy'
self.middleware(req.environ, self.start_fake_response)
self.assertEqual(self.response_status, 200)
def _assert_authorized(self, req, expect_token=True,
account_path='/v1/AUTH_TENANT_ID/'):
self.assertTrue(
req.path.startswith(account_path),
'%r does not start with %r' % (req.path, account_path))
expected_headers = {
'X-Identity-Status': 'Confirmed',
'X-Roles': 'swift-user,_member_',
'X-User-Id': 'USER_ID',
'X-User-Name': 'S3_USER',
'X-Tenant-Id': 'TENANT_ID',
'X-Tenant-Name': 'TENANT_NAME',
'X-Project-Id': 'TENANT_ID',
'X-Project-Name': 'TENANT_NAME',
'X-Auth-Token': 'TOKEN_ID',
}
for header, value in expected_headers.items():
if header == 'X-Auth-Token' and not expect_token:
self.assertNotIn(header, req.headers)
continue
self.assertIn(header, req.headers)
self.assertEqual(value, req.headers[header])
# WSGI wants native strings for headers
self.assertIsInstance(req.headers[header], str)
self.assertEqual(1, self.middleware._app.calls)
self.assertEqual(1, self.requests_mock.call_count)
request_call = self.requests_mock.request_history[0]
self.assertEqual(json.loads(request_call.body), {'credentials': {
'access': 'access',
'signature': 'signature',
'token': base64.urlsafe_b64encode(b'token').decode('ascii')}})
def test_authorized(self):
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',
}
req.get_response(self.middleware)
self._assert_authorized(req)
def test_tolerate_missing_token_id(self):
resp = copy.deepcopy(GOOD_RESPONSE_V2)
del resp['access']['token']['id']
self.requests_mock.post(self.TEST_URL,
status_code=201,
json=resp)
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',
}
req.get_response(self.middleware)
self._assert_authorized(req, expect_token=False)
def test_authorized_bytes(self):
req = Request.blank('/v1/AUTH_cfa/c/o')
req.environ['s3api.auth_details'] = {
'access_key': b'access',
'signature': b'signature',
'string_to_sign': b'token',
}
req.get_response(self.middleware)
self._assert_authorized(req)
def test_authorized_http(self):
auth_uri = 'http://fakehost:35357/v2.0'
self.requests_mock.post(
'%s/s3tokens' % auth_uri,
status_code=201, json=GOOD_RESPONSE_V2)
self.middleware = s3token.filter_factory({
'auth_uri': auth_uri})(self.app)
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',
}
req.get_response(self.middleware)
self._assert_authorized(req)
def test_authorized_v3(self):
# Prior to https://github.com/openstack/keystone/commit/dd1e705
# even v3 URLs would respond with a v2-format response
auth_uri = 'http://fakehost:35357/v3'
self.requests_mock.post(
'%s/s3tokens' % auth_uri,
status_code=201, json=GOOD_RESPONSE_V2)
self.middleware = s3token.filter_factory({
'auth_uri': auth_uri})(self.app)
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',
}
req.get_response(self.middleware)
self._assert_authorized(req)
def test_authorized_trailing_slash(self):
self.middleware = s3token.filter_factory({
'auth_uri': self.TEST_AUTH_URI + '/'})(self.app)
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',
}
req.get_response(self.middleware)
self._assert_authorized(req)
def test_authorization_nova_toconnect(self):
req = Request.blank('/v1/AUTH_swiftint/c/o')
req.environ['s3api.auth_details'] = {
'access_key': u'access:FORCED_TENANT_ID',
'signature': u'signature',
'string_to_sign': u'token',
}
req.get_response(self.middleware)
self._assert_authorized(req, account_path='/v1/AUTH_FORCED_TENANT_ID/')
@mock.patch.object(requests, 'post')
def test_insecure(self, MOCK_REQUEST):
self.middleware = s3token.filter_factory(
{'insecure': 'True', 'auth_uri': 'http://example.com'})(self.app)
text_return_value = json.dumps(GOOD_RESPONSE_V2)
MOCK_REQUEST.return_value = TestResponse({
'status_code': 201,
'text': text_return_value})
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',
}
req.get_response(self.middleware)
self.assertTrue(MOCK_REQUEST.called)
mock_args, mock_kwargs = MOCK_REQUEST.call_args
self.assertIs(mock_kwargs['verify'], False)
def test_insecure_option(self):
# insecure is passed as a string.
# Some non-secure values.
true_values = ['true', 'True', '1', 'yes']
for val in true_values:
config = {'insecure': val,
'certfile': 'false_ind',
'auth_uri': 'http://example.com'}
middleware = s3token.filter_factory(config)(self.app)
self.assertIs(False, middleware._verify)
# Some "secure" values, including unexpected value.
false_values = ['false', 'False', '0', 'no', 'someweirdvalue']
for val in false_values:
config = {'insecure': val,
'certfile': 'false_ind',
'auth_uri': 'http://example.com'}
middleware = s3token.filter_factory(config)(self.app)
self.assertEqual('false_ind', middleware._verify)
# Default is secure.
config = {'certfile': 'false_ind',
'auth_uri': 'http://example.com'}
middleware = s3token.filter_factory(config)(self.app)
self.assertIs('false_ind', middleware._verify)
def test_auth_uris(self):
for conf, expected in [
({'auth_uri': 'https://example.com/v2.0'},
'https://example.com/v2.0/s3tokens'),
# Trailing slash doesn't interfere
({'auth_uri': 'https://example.com/v2.0/'},
'https://example.com/v2.0/s3tokens'),
# keystone running under mod_wsgi often has a path prefix
({'auth_uri': 'https://example.com/identity/v2.0'},
'https://example.com/identity/v2.0/s3tokens'),
({'auth_uri': 'https://example.com/identity/v2.0/'},
'https://example.com/identity/v2.0/s3tokens'),
# IPv4 addresses are fine
({'auth_uri': 'http://127.0.0.1:35357/v3'},
'http://127.0.0.1:35357/v3/s3tokens'),
({'auth_uri': 'http://127.0.0.1:35357/v3/'},
'http://127.0.0.1:35357/v3/s3tokens'),
# IPv6 addresses need [brackets] per RFC 3986
({'auth_uri': 'https://[::FFFF:129.144.52.38]:5000/v3'},
'https://[::FFFF:129.144.52.38]:5000/v3/s3tokens'),
({'auth_uri': 'https://[::FFFF:129.144.52.38]:5000/v3/'},
'https://[::FFFF:129.144.52.38]:5000/v3/s3tokens'),
]:
middleware = s3token.filter_factory(conf)(self.app)
self.assertEqual(expected, middleware._request_uri)
@mock.patch.object(requests, 'post')
def test_http_timeout(self, MOCK_REQUEST):
self.middleware = s3token.filter_factory({
'http_timeout': '2',
'auth_uri': 'http://example.com',
})(FakeApp())
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',
}
req.get_response(self.middleware)
self.assertTrue(MOCK_REQUEST.called)
mock_args, mock_kwargs = MOCK_REQUEST.call_args
self.assertEqual(mock_kwargs['timeout'], 2)
def test_http_timeout_option(self):
good_values = ['1', '5.3', '10', '.001']
for val in good_values:
middleware = s3token.filter_factory({
'http_timeout': val,
'auth_uri': 'http://example.com',
})(FakeApp())
self.assertEqual(float(val), middleware._timeout)
bad_values = ['1, 4', '-3', '100', 'foo', '0']
for val in bad_values:
with self.assertRaises(ValueError) as ctx:
s3token.filter_factory({
'http_timeout': val,
'auth_uri': 'http://example.com',
})(FakeApp())
self.assertTrue(ctx.exception.args[0].startswith((
'invalid literal for float():',
'could not convert string to float:',
'http_timeout must be between 0 and 60 seconds',
)), 'Unexpected error message: %s' % ctx.exception)
# default is 10 seconds
middleware = s3token.filter_factory({
'auth_uri': 'http://example.com'})(FakeApp())
self.assertEqual(10, middleware._timeout)
def test_bad_auth_uris(self):
for auth_uri in [
'/not/a/uri',
'http://',
'//example.com/path']:
with self.assertRaises(ConfigFileError) as cm:
s3token.filter_factory({'auth_uri': auth_uri})(self.app)
self.assertEqual('Invalid auth_uri; must include scheme and host',
cm.exception.message)
with self.assertRaises(ConfigFileError) as cm:
s3token.filter_factory({
'auth_uri': 'nonhttp://example.com'})(self.app)
self.assertEqual('Invalid auth_uri; scheme must be http or https',
cm.exception.message)
for auth_uri in [
'http://user@example.com/',
'http://example.com/?with=query',
'http://example.com/#with-fragment']:
with self.assertRaises(ConfigFileError) as cm:
s3token.filter_factory({'auth_uri': auth_uri})(self.app)
self.assertEqual('Invalid auth_uri; must not include username, '
'query, or fragment', cm.exception.message)
def test_unicode_path(self):
url = u'/v1/AUTH_cfa/c/euro\u20ac'.encode('utf8')
req = Request.blank(urllib.parse.quote(url))
req.environ['s3api.auth_details'] = {
'access_key': u'access',
'signature': u'signature',
'string_to_sign': u'token',
}
req.get_response(self.middleware)
self._assert_authorized(req)
def test_authorize_with_access_key(self):
req = Request.blank('/v1/accesskey/c/o')
req.environ['s3api.auth_details'] = {
'access_key': u'access',
'signature': u'signature',
'string_to_sign': u'token',
}
req.get_response(self.middleware)
self._assert_authorized(req, account_path='/v1/')
self.assertEqual(req.environ['PATH_INFO'], '/v1/AUTH_TENANT_ID/c/o')
def test_authorize_with_access_key_and_unquote_chars(self):
req = Request.blank('/v1/access%key=/c/o')
req.environ['s3api.auth_details'] = {
'access_key': u'access',
'signature': u'signature',
'string_to_sign': u'token',
}
req.get_response(self.middleware)
self._assert_authorized(req, account_path='/v1/')
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):
def test_unauthorized_token(self):
ret = {"error":
{"message": "EC2 access key not found.",
"code": 401,
"title": "Unauthorized"}}
self.requests_mock.post(self.TEST_URL, status_code=403, json=ret)
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',
}
resp = req.get_response(self.middleware)
s3_denied_req = self.middleware._deny_request('AccessDenied')
self.assertEqual(resp.body, s3_denied_req.body)
self.assertEqual(
resp.status_int, # pylint: disable-msg=E1101
s3_denied_req.status_int) # pylint: disable-msg=E1101
self.assertEqual(0, self.middleware._app.calls)
self.assertEqual(1, self.requests_mock.call_count)
request_call = self.requests_mock.request_history[0]
self.assertEqual(json.loads(request_call.body), {'credentials': {
'access': 'access',
'signature': 'signature',
'token': base64.urlsafe_b64encode(b'token').decode('ascii')}})
def test_no_s3_creds_defers_to_auth_middleware(self):
# Without an Authorization header, we should just pass through to the
# auth system to make a decision.
req = Request.blank('/v1/AUTH_cfa/c/o')
resp = req.get_response(self.middleware)
self.assertEqual(resp.status_int, 200) # pylint: disable-msg=E1101
self.assertEqual(1, self.middleware._app.calls)
def test_fail_to_connect_to_keystone(self):
with mock.patch.object(self.middleware, '_json_request') as o:
s3_invalid_resp = self.middleware._deny_request('InvalidURI')
o.side_effect = s3_invalid_resp
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',
}
resp = req.get_response(self.middleware)
self.assertEqual(resp.body, s3_invalid_resp.body)
self.assertEqual(
resp.status_int, # pylint: disable-msg=E1101
s3_invalid_resp.status_int) # pylint: disable-msg=E1101
self.assertEqual(0, self.middleware._app.calls)
def _test_bad_reply(self, response_body):
self.requests_mock.post(self.TEST_URL,
status_code=201,
text=response_body)
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',
}
resp = req.get_response(self.middleware)
s3_invalid_resp = self.middleware._deny_request('InvalidURI')
self.assertEqual(resp.body, s3_invalid_resp.body)
self.assertEqual(
resp.status_int, # pylint: disable-msg=E1101
s3_invalid_resp.status_int) # pylint: disable-msg=E1101
self.assertEqual(0, self.middleware._app.calls)
def test_bad_reply_not_json(self):
self._test_bad_reply('<badreply>')
def _test_bad_reply_missing_parts(self, *parts):
resp = copy.deepcopy(GOOD_RESPONSE_V2)
part_dict = resp
for part in parts[:-1]:
part_dict = part_dict[part]
del part_dict[parts[-1]]
self._test_bad_reply(json.dumps(resp))
def test_bad_reply_missing_token_dict(self):
self._test_bad_reply_missing_parts('access', 'token')
def test_bad_reply_missing_user_dict(self):
self._test_bad_reply_missing_parts('access', 'user')
def test_bad_reply_missing_user_roles(self):
self._test_bad_reply_missing_parts('access', 'user', 'roles')
def test_bad_reply_missing_user_name(self):
self._test_bad_reply_missing_parts('access', 'user', 'name')
def test_bad_reply_missing_user_id(self):
self._test_bad_reply_missing_parts('access', 'user', 'id')
def test_bad_reply_missing_tenant_dict(self):
self._test_bad_reply_missing_parts('access', 'token', 'tenant')
def test_bad_reply_missing_tenant_id(self):
self._test_bad_reply_missing_parts('access', 'token', 'tenant', 'id')
def test_bad_reply_missing_tenant_name(self):
self._test_bad_reply_missing_parts('access', 'token', 'tenant', 'name')
def test_bad_reply_valid_but_bad_json(self):
self._test_bad_reply('{}')
self._test_bad_reply('[]')
self._test_bad_reply('null')
self._test_bad_reply('"foo"')
self._test_bad_reply('1')
self._test_bad_reply('true')
class S3TokenMiddlewareTestDeferredAuth(S3TokenMiddlewareTestBase):
def setUp(self):
super(S3TokenMiddlewareTestDeferredAuth, self).setUp()
self.conf['delay_auth_decision'] = 'yes'
self.middleware = s3token.S3Token(FakeApp(), self.conf)
def test_unauthorized_token(self):
ret = {"error":
{"message": "EC2 access key not found.",
"code": 401,
"title": "Unauthorized"}}
self.requests_mock.post(self.TEST_URL, status_code=403, json=ret)
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',
}
resp = req.get_response(self.middleware)
self.assertEqual(
resp.status_int, # pylint: disable-msg=E1101
200)
self.assertNotIn('X-Auth-Token', req.headers)
self.assertEqual(1, self.middleware._app.calls)
self.assertEqual(1, self.requests_mock.call_count)
request_call = self.requests_mock.request_history[0]
self.assertEqual(json.loads(request_call.body), {'credentials': {
'access': 'access',
'signature': 'signature',
'token': base64.urlsafe_b64encode(b'token').decode('ascii')}})
def test_fail_to_connect_to_keystone(self):
with mock.patch.object(self.middleware, '_json_request') as o:
o.side_effect = self.middleware._deny_request('InvalidURI')
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',
}
resp = req.get_response(self.middleware)
self.assertEqual(
resp.status_int, # pylint: disable-msg=E1101
200)
self.assertNotIn('X-Auth-Token', req.headers)
self.assertEqual(1, self.middleware._app.calls)
def test_bad_reply(self):
self.requests_mock.post(self.TEST_URL,
status_code=201,
text="<badreply>")
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',
}
resp = req.get_response(self.middleware)
self.assertEqual(
resp.status_int, # pylint: disable-msg=E1101
200)
self.assertNotIn('X-Auth-Token', req.headers)
self.assertEqual(1, self.middleware._app.calls)
class S3TokenMiddlewareTestV3(S3TokenMiddlewareTestBase):
def setUp(self):
super(S3TokenMiddlewareTestV3, self).setUp()
self.requests_mock.post(self.TEST_URL,
status_code=200,
json=GOOD_RESPONSE_V3)
def _assert_authorized(self, req,
account_path='/v1/AUTH_PROJECT_ID/'):
self.assertTrue(req.path.startswith(account_path))
expected_headers = {
'X-Identity-Status': 'Confirmed',
'X-Roles': 'swift-user,_member_',
'X-User-Id': 'USER_ID',
'X-User-Name': 'S3_USER',
'X-User-Domain-Id': 'default',
'X-User-Domain-Name': 'Default',
'X-Tenant-Id': 'PROJECT_ID',
'X-Tenant-Name': 'PROJECT_NAME',
'X-Project-Id': 'PROJECT_ID',
'X-Project-Name': 'PROJECT_NAME',
'X-Project-Domain-Id': 'PROJECT_DOMAIN_ID',
'X-Project-Domain-Name': 'PROJECT_DOMAIN_NAME',
}
for header, value in expected_headers.items():
self.assertIn(header, req.headers)
self.assertEqual(value, req.headers[header])
# WSGI wants native strings for headers
self.assertIsInstance(req.headers[header], str)
self.assertNotIn('X-Auth-Token', req.headers)
self.assertEqual(1, self.middleware._app.calls)
def test_authorized(self):
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',
}
req.get_response(self.middleware)
self._assert_authorized(req)
def test_authorized_bytes(self):
req = Request.blank('/v1/AUTH_cfa/c/o')
req.environ['s3api.auth_details'] = {
'access_key': b'access',
'signature': b'signature',
'string_to_sign': b'token',
}
req.get_response(self.middleware)
self._assert_authorized(req)
def test_authorized_http(self):
# Following https://github.com/openstack/keystone/commit/3ec1aa4
# even v2 URLs would respond with a v3-format response
auth_uri = 'http://fakehost:35357/v2.0/'
self.requests_mock.post(
auth_uri + 's3tokens',
status_code=201, json=GOOD_RESPONSE_V3)
self.middleware = s3token.filter_factory({
'auth_uri': auth_uri})(self.app)
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',
}
req.get_response(self.middleware)
self._assert_authorized(req)
def test_authorized_v3(self):
auth_uri = 'http://fakehost:35357/v3/'
self.requests_mock.post(
auth_uri + 's3tokens',
status_code=201, json=GOOD_RESPONSE_V3)
self.middleware = s3token.filter_factory({
'auth_uri': auth_uri})(self.app)
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',
}
req.get_response(self.middleware)
self._assert_authorized(req)
def test_authorized_trailing_slash(self):
self.middleware = s3token.filter_factory({
'auth_uri': self.TEST_AUTH_URI + '/'})(self.app)
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',
}
req.get_response(self.middleware)
self._assert_authorized(req)
def test_authorization_nova_toconnect(self):
req = Request.blank('/v1/AUTH_swiftint/c/o')
req.environ['s3api.auth_details'] = {
'access_key': u'access:FORCED_TENANT_ID',
'signature': u'signature',
'string_to_sign': u'token',
}
req.get_response(self.middleware)
self._assert_authorized(req, account_path='/v1/AUTH_FORCED_TENANT_ID/')
def _test_bad_reply_missing_parts(self, *parts):
resp = copy.deepcopy(GOOD_RESPONSE_V3)
part_dict = resp
for part in parts[:-1]:
part_dict = part_dict[part]
del part_dict[parts[-1]]
self.requests_mock.post(self.TEST_URL,
status_code=201,
text=json.dumps(resp))
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',
}
resp = req.get_response(self.middleware)
s3_invalid_resp = self.middleware._deny_request('InvalidURI')
self.assertEqual(resp.body, s3_invalid_resp.body)
self.assertEqual(
resp.status_int, # pylint: disable-msg=E1101
s3_invalid_resp.status_int) # pylint: disable-msg=E1101
self.assertEqual(0, self.middleware._app.calls)
def test_bad_reply_missing_parts(self):
self._test_bad_reply_missing_parts('token', 'user', 'id')
self._test_bad_reply_missing_parts('token', 'user', 'name')
self._test_bad_reply_missing_parts('token', 'user', 'domain', 'id')
self._test_bad_reply_missing_parts('token', 'user', 'domain', 'name')
self._test_bad_reply_missing_parts('token', 'user', 'domain')
self._test_bad_reply_missing_parts('token', 'user')
self._test_bad_reply_missing_parts('token', 'project', 'id')
self._test_bad_reply_missing_parts('token', 'project', 'name')
self._test_bad_reply_missing_parts('token', 'project', 'domain', 'id')
self._test_bad_reply_missing_parts('token', 'project', 'domain',
'name')
self._test_bad_reply_missing_parts('token', 'project', 'domain')
self._test_bad_reply_missing_parts('token', 'project')
self._test_bad_reply_missing_parts('token', 'roles')
def test_authorize_with_access_key(self):
req = Request.blank('/v1/accesskey/c/o')
req.environ['s3api.auth_details'] = {
'access_key': u'access',
'signature': u'signature',
'string_to_sign': u'token',
}
req.get_response(self.middleware)
self._assert_authorized(req, account_path='/v1/')
self.assertEqual(req.environ['PATH_INFO'], '/v1/AUTH_PROJECT_ID/c/o')
def test_authorize_with_access_key_and_unquote_chars(self):
req = Request.blank('/v1/ab%c=/c/o')
req.environ['s3api.auth_details'] = {
'access_key': u'access',
'signature': u'signature',
'string_to_sign': u'token',
}
req.get_response(self.middleware)
self._assert_authorized(req, account_path='/v1/')
self.assertEqual(req.environ['PATH_INFO'], '/v1/AUTH_PROJECT_ID/c/o')