# Copyright (c) 2014 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 binascii import unittest from datetime import datetime from hashlib import sha256 import os from os.path import join import time from mock import patch import six import json from swift.common import swob from swift.common.swob import Request from swift.common.middleware.proxy_logging import ProxyLoggingMiddleware from test.unit.common.middleware.s3api import S3ApiTestCase from test.unit.common.middleware.s3api.test_s3_acl import s3acl from swift.common.middleware.s3api.s3request import SigV4Request from swift.common.middleware.s3api.subresource import ACL, User, encode_acl, \ Owner, Grant from swift.common.middleware.s3api.etree import fromstring from swift.common.middleware.s3api.utils import mktime, S3Timestamp from swift.common.middleware.versioned_writes.object_versioning import \ DELETE_MARKER_CONTENT_TYPE from swift.common.utils import md5 class TestS3ApiObj(S3ApiTestCase): def setUp(self): super(TestS3ApiObj, self).setUp() self.object_body = b'hello' self.etag = md5(self.object_body, usedforsecurity=False).hexdigest() self.last_modified = 'Fri, 01 Apr 2014 12:00:00 GMT' self.response_headers = {'Content-Type': 'text/html', 'Content-Length': len(self.object_body), 'Content-Disposition': 'inline', 'Content-Language': 'en', 'x-object-meta-test': 'swift', 'etag': self.etag, 'last-modified': self.last_modified, 'expires': 'Mon, 21 Sep 2015 12:00:00 GMT', 'x-robots-tag': 'nofollow', 'cache-control': 'private'} self.swift.register('GET', '/v1/AUTH_test/bucket/object', swob.HTTPOk, self.response_headers, self.object_body) self.swift.register('GET', '/v1/AUTH_test/bucket/object?symlink=get', swob.HTTPOk, self.response_headers, self.object_body) self.swift.register('PUT', '/v1/AUTH_test/bucket/object', swob.HTTPCreated, {'etag': self.etag, 'last-modified': self.last_modified, 'x-object-meta-something': 'oh hai'}, None) def _test_object_GETorHEAD(self, method): req = Request.blank('/bucket/object', environ={'REQUEST_METHOD': method}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '200') # we'll want this for logging self.assertEqual(req.headers['X-Backend-Storage-Policy-Index'], '2') unexpected_headers = [] for key, val in self.response_headers.items(): if key in ('Content-Length', 'Content-Type', 'content-encoding', 'last-modified', 'cache-control', 'Content-Disposition', 'Content-Language', 'expires', 'x-robots-tag'): self.assertIn(key, headers) self.assertEqual(headers[key], str(val)) elif key == 'etag': self.assertEqual(headers[key], '"%s"' % val) elif key.startswith('x-object-meta-'): self.assertIn('x-amz-meta-' + key[14:], headers) self.assertEqual(headers['x-amz-meta-' + key[14:]], val) else: unexpected_headers.append((key, val)) if unexpected_headers: self.fail('unexpected headers: %r' % unexpected_headers) self.assertEqual(headers['etag'], '"%s"' % self.response_headers['etag']) if method == 'GET': self.assertEqual(body, self.object_body) @s3acl def test_object_HEAD_error(self): # HEAD does not return the body even an error response in the # specifications of the REST API. # So, check the response code for error test of HEAD. req = Request.blank('/bucket/object', environ={'REQUEST_METHOD': 'HEAD'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) self.swift.register('HEAD', '/v1/AUTH_test/bucket/object', swob.HTTPUnauthorized, {}, None) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '403') self.assertEqual(body, b'') # sanity req = Request.blank('/bucket/object', environ={'REQUEST_METHOD': 'HEAD'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) self.swift.register('HEAD', '/v1/AUTH_test/bucket/object', swob.HTTPForbidden, {}, None) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '403') self.assertEqual(body, b'') # sanity req = Request.blank('/bucket/object', environ={'REQUEST_METHOD': 'HEAD'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) self.swift.register('HEAD', '/v1/AUTH_test/bucket/object', swob.HTTPNotFound, {}, None) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '404') self.assertEqual(body, b'') # sanity req = Request.blank('/bucket/object', environ={'REQUEST_METHOD': 'HEAD'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) self.swift.register('HEAD', '/v1/AUTH_test/bucket/object', swob.HTTPPreconditionFailed, {}, None) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '412') self.assertEqual(body, b'') # sanity req = Request.blank('/bucket/object', environ={'REQUEST_METHOD': 'HEAD'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) self.swift.register('HEAD', '/v1/AUTH_test/bucket/object', swob.HTTPServerError, {}, None) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '500') self.assertEqual(body, b'') # sanity req = Request.blank('/bucket/object', environ={'REQUEST_METHOD': 'HEAD'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) self.swift.register('HEAD', '/v1/AUTH_test/bucket/object', swob.HTTPServiceUnavailable, {}, None) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '503') self.assertEqual(body, b'') # sanity def test_object_HEAD(self): self._test_object_GETorHEAD('HEAD') def test_object_policy_index_logging(self): req = Request.blank('/bucket/object', headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) self.s3api = ProxyLoggingMiddleware(self.s3api, {}, logger=self.logger) status, headers, body = self.call_s3api(req) access_lines = self.logger.get_lines_for_level('info') self.assertEqual(1, len(access_lines)) parts = access_lines[0].split() self.assertEqual(' '.join(parts[3:7]), 'GET /bucket/object HTTP/1.0 200') self.assertEqual(parts[-1], '2') def _test_object_HEAD_Range(self, range_value): req = Request.blank('/bucket/object', environ={'REQUEST_METHOD': 'HEAD'}, headers={'Authorization': 'AWS test:tester:hmac', 'Range': range_value, 'Date': self.get_date_header()}) return self.call_s3api(req) @s3acl def test_object_HEAD_Range_with_invalid_value(self): range_value = '' status, headers, body = self._test_object_HEAD_Range(range_value) self.assertEqual(status.split()[0], '200') self.assertTrue('content-length' in headers) self.assertEqual(headers['content-length'], '5') self.assertTrue('content-range' not in headers) range_value = 'hoge' status, headers, body = self._test_object_HEAD_Range(range_value) self.assertEqual(status.split()[0], '200') self.assertTrue('content-length' in headers) self.assertEqual(headers['content-length'], '5') self.assertTrue('content-range' not in headers) range_value = 'bytes=' status, headers, body = self._test_object_HEAD_Range(range_value) self.assertEqual(status.split()[0], '200') self.assertTrue('content-length' in headers) self.assertEqual(headers['content-length'], '5') self.assertTrue('content-range' not in headers) range_value = 'bytes=1' status, headers, body = self._test_object_HEAD_Range(range_value) self.assertEqual(status.split()[0], '200') self.assertTrue('content-length' in headers) self.assertEqual(headers['content-length'], '5') self.assertTrue('content-range' not in headers) range_value = 'bytes=5-1' status, headers, body = self._test_object_HEAD_Range(range_value) self.assertEqual(status.split()[0], '200') self.assertTrue('content-length' in headers) self.assertEqual(headers['content-length'], '5') self.assertTrue('content-range' not in headers) range_value = 'bytes=5-10' status, headers, body = self._test_object_HEAD_Range(range_value) self.assertEqual(status.split()[0], '416') @s3acl def test_object_HEAD_Range(self): # update response headers self.swift.register('HEAD', '/v1/AUTH_test/bucket/object', swob.HTTPOk, self.response_headers, self.object_body) range_value = 'bytes=0-3' status, headers, body = self._test_object_HEAD_Range(range_value) self.assertEqual(status.split()[0], '206') self.assertTrue('content-length' in headers) self.assertEqual(headers['content-length'], '4') self.assertTrue('content-range' in headers) self.assertTrue(headers['content-range'].startswith('bytes 0-3')) self.assertTrue('x-amz-meta-test' in headers) self.assertEqual('swift', headers['x-amz-meta-test']) range_value = 'bytes=3-3' status, headers, body = self._test_object_HEAD_Range(range_value) self.assertEqual(status.split()[0], '206') self.assertTrue('content-length' in headers) self.assertEqual(headers['content-length'], '1') self.assertTrue('content-range' in headers) self.assertTrue(headers['content-range'].startswith('bytes 3-3')) self.assertTrue('x-amz-meta-test' in headers) self.assertEqual('swift', headers['x-amz-meta-test']) range_value = 'bytes=1-' status, headers, body = self._test_object_HEAD_Range(range_value) self.assertEqual(status.split()[0], '206') self.assertTrue('content-length' in headers) self.assertEqual(headers['content-length'], '4') self.assertTrue('content-range' in headers) self.assertTrue(headers['content-range'].startswith('bytes 1-4')) self.assertTrue('x-amz-meta-test' in headers) self.assertEqual('swift', headers['x-amz-meta-test']) range_value = 'bytes=-3' status, headers, body = self._test_object_HEAD_Range(range_value) self.assertEqual(status.split()[0], '206') self.assertTrue('content-length' in headers) self.assertEqual(headers['content-length'], '3') self.assertTrue('content-range' in headers) self.assertTrue(headers['content-range'].startswith('bytes 2-4')) self.assertTrue('x-amz-meta-test' in headers) self.assertEqual('swift', headers['x-amz-meta-test']) @s3acl def test_object_GET_error(self): code = self._test_method_error('GET', '/bucket/object', swob.HTTPUnauthorized) self.assertEqual(code, 'SignatureDoesNotMatch') code = self._test_method_error('GET', '/bucket/object', swob.HTTPForbidden) self.assertEqual(code, 'AccessDenied') code = self._test_method_error('GET', '/bucket/object', swob.HTTPNotFound) self.assertEqual(code, 'NoSuchKey') code = self._test_method_error('GET', '/bucket/object', swob.HTTPServerError) self.assertEqual(code, 'InternalError') code = self._test_method_error('GET', '/bucket/object', swob.HTTPPreconditionFailed) self.assertEqual(code, 'PreconditionFailed') code = self._test_method_error('GET', '/bucket/object', swob.HTTPServiceUnavailable) self.assertEqual(code, 'ServiceUnavailable') @s3acl def test_object_GET(self): self._test_object_GETorHEAD('GET') @s3acl(s3acl_only=True) def test_object_GET_with_s3acl_and_unknown_user(self): self.swift.remote_user = None req = Request.blank('/bucket/object', environ={'REQUEST_METHOD': 'GET'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) status, headers, body = self.call_s3api(req) self.assertEqual(status, '403 Forbidden') self.assertEqual(self._get_error_code(body), 'SignatureDoesNotMatch') @s3acl(s3acl_only=True) def test_object_GET_with_s3acl_and_keystone(self): # for passing keystone authentication root orig_auth = self.swift._fake_auth_middleware calls = [] def wrapped_auth(env): calls.append((env['REQUEST_METHOD'], 's3api.auth_details' in env)) orig_auth(env) with patch.object(self.swift, '_fake_auth_middleware', wrapped_auth): self._test_object_GETorHEAD('GET') self.assertEqual(calls, [ ('TEST', True), ('HEAD', False), ('GET', False), ]) @s3acl def test_object_GET_Range(self): req = Request.blank('/bucket/object', environ={'REQUEST_METHOD': 'GET'}, headers={'Authorization': 'AWS test:tester:hmac', 'Range': 'bytes=0-3', 'Date': self.get_date_header()}) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '206') self.assertTrue('content-range' in headers) self.assertTrue(headers['content-range'].startswith('bytes 0-3')) @s3acl def test_object_GET_Range_error(self): code = self._test_method_error('GET', '/bucket/object', swob.HTTPRequestedRangeNotSatisfiable) self.assertEqual(code, 'InvalidRange') @s3acl def test_object_GET_Response(self): req = Request.blank('/bucket/object', environ={'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'response-content-type=%s&' 'response-content-language=%s&' 'response-expires=%s&' 'response-cache-control=%s&' 'response-content-disposition=%s&' 'response-content-encoding=%s&' % ('text/plain', 'en', 'Fri, 01 Apr 2014 12:00:00 GMT', 'no-cache', 'attachment', 'gzip')}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '200') self.assertTrue('content-type' in headers) self.assertEqual(headers['content-type'], 'text/plain') self.assertTrue('content-language' in headers) self.assertEqual(headers['content-language'], 'en') self.assertTrue('expires' in headers) self.assertEqual(headers['expires'], 'Fri, 01 Apr 2014 12:00:00 GMT') self.assertTrue('cache-control' in headers) self.assertEqual(headers['cache-control'], 'no-cache') self.assertTrue('content-disposition' in headers) self.assertEqual(headers['content-disposition'], 'attachment') self.assertTrue('content-encoding' in headers) self.assertEqual(headers['content-encoding'], 'gzip') @s3acl def test_object_GET_version_id_not_implemented(self): # GET version that is not null req = Request.blank('/bucket/object?versionId=2', environ={'REQUEST_METHOD': 'GET'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) with patch('swift.common.middleware.s3api.controllers.obj.' 'get_swift_info', return_value={}): status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '501', body) # GET current version req = Request.blank('/bucket/object?versionId=null', environ={'REQUEST_METHOD': 'GET'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) with patch('swift.common.middleware.s3api.controllers.obj.' 'get_swift_info', return_value={}): status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '200', body) self.assertEqual(body, self.object_body) @s3acl def test_object_GET_version_id(self): # GET current version req = Request.blank('/bucket/object?versionId=null', environ={'REQUEST_METHOD': 'GET'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '200', body) self.assertEqual(body, self.object_body) # GET current version that is not null req = Request.blank('/bucket/object?versionId=2', environ={'REQUEST_METHOD': 'GET'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '200', body) self.assertEqual(body, self.object_body) # GET version in archive headers = self.response_headers.copy() headers['Content-Length'] = 6 account = 'test:tester' grants = [Grant(User(account), 'FULL_CONTROL')] headers.update( encode_acl('object', ACL(Owner(account, account), grants))) self.swift.register( 'HEAD', '/v1/AUTH_test/bucket/object?version-id=1', swob.HTTPOk, headers, None) self.swift.register( 'GET', '/v1/AUTH_test/bucket/object?version-id=1', swob.HTTPOk, headers, 'hello1') req = Request.blank('/bucket/object?versionId=1', environ={'REQUEST_METHOD': 'GET'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '200', body) self.assertEqual(body, b'hello1') # Version not found self.swift.register( 'GET', '/v1/AUTH_test/bucket/object?version-id=A', swob.HTTPNotFound, {}, None) req = Request.blank('/bucket/object?versionId=A', environ={'REQUEST_METHOD': 'GET'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '404') @s3acl(versioning_enabled=False) def test_object_GET_with_version_id_but_not_enabled(self): # Version not found self.swift.register( 'HEAD', '/v1/AUTH_test/bucket', swob.HTTPNoContent, {}, None) req = Request.blank('/bucket/object?versionId=A', environ={'REQUEST_METHOD': 'GET'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header()}) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '404') elem = fromstring(body, 'Error') self.assertEqual(elem.find('Code').text, 'NoSuchVersion') self.assertEqual(elem.find('Key').text, 'object') self.assertEqual(elem.find('VersionId').text, 'A') expected_calls = [] # NB: No actual backend GET! self.assertEqual(expected_calls, self.swift.calls) @s3acl def test_object_PUT_error(self): code = self._test_method_error('PUT', '/bucket/object', swob.HTTPUnauthorized) self.assertEqual(code, 'SignatureDoesNotMatch') code = self._test_method_error('PUT', '/bucket/object', swob.HTTPForbidden) self.assertEqual(code, 'AccessDenied') code = self._test_method_error('PUT', '/bucket/object', swob.HTTPNotFound) self.assertEqual(code, 'NoSuchBucket') code = self._test_method_error('PUT', '/bucket/object', swob.HTTPRequestEntityTooLarge) self.assertEqual(code, 'EntityTooLarge') code = self._test_method_error('PUT', '/bucket/object', swob.HTTPServerError) self.assertEqual(code, 'InternalError') code = self._test_method_error('PUT', '/bucket/object', swob.HTTPUnprocessableEntity) self.assertEqual(code, 'BadDigest') code = self._test_method_error('PUT', '/bucket/object', swob.HTTPLengthRequired) self.assertEqual(code, 'MissingContentLength') # Swift can 412 if the versions container is missing code = self._test_method_error('PUT', '/bucket/object', swob.HTTPPreconditionFailed) self.assertEqual(code, 'PreconditionFailed') code = self._test_method_error('PUT', '/bucket/object', swob.HTTPServiceUnavailable) self.assertEqual(code, 'ServiceUnavailable') code = self._test_method_error('PUT', '/bucket/object', swob.HTTPCreated, {'X-Amz-Copy-Source': ''}) self.assertEqual(code, 'InvalidArgument') code = self._test_method_error('PUT', '/bucket/object', swob.HTTPCreated, {'X-Amz-Copy-Source': '/'}) self.assertEqual(code, 'InvalidArgument') code = self._test_method_error('PUT', '/bucket/object', swob.HTTPCreated, {'X-Amz-Copy-Source': '/bucket'}) self.assertEqual(code, 'InvalidArgument') code = self._test_method_error('PUT', '/bucket/object', swob.HTTPCreated, {'X-Amz-Copy-Source': '/bucket/'}) self.assertEqual(code, 'InvalidArgument') code = self._test_method_error( 'PUT', '/bucket/object', swob.HTTPCreated, {'X-Amz-Copy-Source': '/bucket/src_obj?foo=bar'}) self.assertEqual(code, 'InvalidArgument') # adding other query paramerters will cause an error code = self._test_method_error( 'PUT', '/bucket/object', swob.HTTPCreated, {'X-Amz-Copy-Source': '/bucket/src_obj?versionId=foo&bar=baz'}) self.assertEqual(code, 'InvalidArgument') # ...even versionId appears in the last code = self._test_method_error( 'PUT', '/bucket/object', swob.HTTPCreated, {'X-Amz-Copy-Source': '/bucket/src_obj?bar=baz&versionId=foo'}) self.assertEqual(code, 'InvalidArgument') code = self._test_method_error( 'PUT', '/bucket/object', swob.HTTPCreated, {'X-Amz-Copy-Source': '/src_bucket/src_object', 'X-Amz-Copy-Source-Range': 'bytes=0-0'}) self.assertEqual(code, 'InvalidArgument') code = self._test_method_error('PUT', '/bucket/object', swob.HTTPRequestTimeout) self.assertEqual(code, 'RequestTimeout') def test_object_PUT_with_version(self): self.swift.register('GET', '/v1/AUTH_test/bucket/src_obj?version-id=foo', swob.HTTPOk, self.response_headers, self.object_body) self.swift.register('PUT', '/v1/AUTH_test/bucket/object', swob.HTTPCreated, { 'etag': self.etag, 'last-modified': self.last_modified, }, None) req = Request.blank('/bucket/object', method='PUT', body='', headers={ 'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header(), 'X-Amz-Copy-Source': '/bucket/src_obj?versionId=foo', }) status, headers, body = self.call_s3api(req) self.assertEqual('200 OK', status) elem = fromstring(body, 'CopyObjectResult') self.assertEqual(elem.find('ETag').text, '"%s"' % self.etag) self.assertEqual(self.swift.calls, [ ('HEAD', '/v1/AUTH_test/bucket/src_obj?version-id=foo'), ('PUT', '/v1/AUTH_test/bucket/object?version-id=foo'), ]) _, _, headers = self.swift.calls_with_headers[-1] self.assertEqual(headers['x-copy-from'], '/bucket/src_obj') @s3acl def test_object_PUT(self): etag = self.response_headers['etag'] content_md5 = binascii.b2a_base64(binascii.a2b_hex(etag)).strip() if not six.PY2: content_md5 = content_md5.decode('ascii') req = Request.blank( '/bucket/object', environ={'REQUEST_METHOD': 'PUT'}, headers={'Authorization': 'AWS test:tester:hmac', 'x-amz-storage-class': 'STANDARD', 'Content-MD5': content_md5, 'Date': self.get_date_header()}, body=self.object_body) req.date = datetime.now() req.content_type = 'text/plain' status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '200') # Check that s3api returns an etag header. self.assertEqual(headers['etag'], '"%s"' % etag) _, _, headers = self.swift.calls_with_headers[-1] # Check that s3api converts a Content-MD5 header into an etag. self.assertEqual(headers['etag'], etag) @s3acl def test_object_PUT_quota_exceeded(self): etag = self.response_headers['etag'] content_md5 = binascii.b2a_base64(binascii.a2b_hex(etag)).strip() if not six.PY2: content_md5 = content_md5.decode('ascii') self.swift.register( 'PUT', '/v1/AUTH_test/bucket/object', swob.HTTPRequestEntityTooLarge, {}, 'Upload exceeds quota.') req = Request.blank( '/bucket/object', environ={'REQUEST_METHOD': 'PUT'}, headers={'Authorization': 'AWS test:tester:hmac', 'x-amz-storage-class': 'STANDARD', 'Content-MD5': content_md5, 'Date': self.get_date_header()}, body=self.object_body) req.date = datetime.now() req.content_type = 'text/plain' status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '400') self.assertIn(b'EntityTooLarge', body) self.assertIn(b'Upload exceeds quota.') req = Request.blank('/bucket/object', environ={'REQUEST_METHOD': 'DELETE'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header(), 'Content-Type': 'foo/bar'}) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '204') self.assertEqual(body, b'') self.assertIn(('HEAD', '/v1/AUTH_test/bucket/object?symlink=get'), self.swift.calls) self.assertIn(('DELETE', '/v1/AUTH_test/bucket/object' '?multipart-manifest=delete'), self.swift.calls) _, path, headers = self.swift.calls_with_headers[-1] path, query_string = path.split('?', 1) query = {} for q in query_string.split('&'): key, arg = q.split('=') query[key] = arg self.assertEqual(query['multipart-manifest'], 'delete') # HEAD did not indicate that it was an S3 MPU, so no async delete self.assertNotIn('async', query) self.assertNotIn('Content-Type', headers) @s3acl def test_slo_object_async_DELETE(self): self.swift.register('HEAD', '/v1/AUTH_test/bucket/object', swob.HTTPOk, {'x-static-large-object': 'True', 'x-object-sysmeta-s3api-etag': 's3-style-etag'}, None) self.swift.register('DELETE', '/v1/AUTH_test/bucket/object', swob.HTTPOk, {}, '') req = Request.blank('/bucket/object', environ={'REQUEST_METHOD': 'DELETE'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header(), 'Content-Type': 'foo/bar'}) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '204') self.assertEqual(body, b'') self.assertIn(('HEAD', '/v1/AUTH_test/bucket/object?symlink=get'), self.swift.calls) self.assertIn(('DELETE', '/v1/AUTH_test/bucket/object' '?async=on&multipart-manifest=delete'), self.swift.calls) _, path, headers = self.swift.calls_with_headers[-1] path, query_string = path.split('?', 1) query = {} for q in query_string.split('&'): key, arg = q.split('=') query[key] = arg self.assertEqual(query['multipart-manifest'], 'delete') self.assertEqual(query['async'], 'on') self.assertNotIn('Content-Type', headers) def _test_object_for_s3acl(self, method, account): req = Request.blank('/bucket/object', environ={'REQUEST_METHOD': method}, headers={'Authorization': 'AWS %s:hmac' % account, 'Date': self.get_date_header()}) return self.call_s3api(req) def _test_set_container_permission(self, account, permission): grants = [Grant(User(account), permission)] headers = \ encode_acl('container', ACL(Owner('test:tester', 'test:tester'), grants)) self.swift.register('HEAD', '/v1/AUTH_test/bucket', swob.HTTPNoContent, headers, None) @s3acl(s3acl_only=True) def test_object_GET_without_permission(self): status, headers, body = self._test_object_for_s3acl('GET', 'test:other') self.assertEqual(self._get_error_code(body), 'AccessDenied') @s3acl(s3acl_only=True) def test_object_GET_with_read_permission(self): status, headers, body = self._test_object_for_s3acl('GET', 'test:read') self.assertEqual(status.split()[0], '200') @s3acl(s3acl_only=True) def test_object_GET_with_fullcontrol_permission(self): status, headers, body = \ self._test_object_for_s3acl('GET', 'test:full_control') self.assertEqual(status.split()[0], '200') @s3acl(s3acl_only=True) def test_object_PUT_without_permission(self): status, headers, body = self._test_object_for_s3acl('PUT', 'test:other') self.assertEqual(self._get_error_code(body), 'AccessDenied') @s3acl(s3acl_only=True) def test_object_PUT_with_owner_permission(self): status, headers, body = self._test_object_for_s3acl('PUT', 'test:tester') self.assertEqual(status.split()[0], '200') @s3acl(s3acl_only=True) def test_object_PUT_with_write_permission(self): account = 'test:other' self._test_set_container_permission(account, 'WRITE') status, headers, body = self._test_object_for_s3acl('PUT', account) self.assertEqual(status.split()[0], '200') @s3acl(s3acl_only=True) def test_object_PUT_with_fullcontrol_permission(self): account = 'test:other' self._test_set_container_permission(account, 'FULL_CONTROL') status, headers, body = \ self._test_object_for_s3acl('PUT', account) self.assertEqual(status.split()[0], '200') @s3acl(s3acl_only=True) def test_object_DELETE_without_permission(self): account = 'test:other' status, headers, body = self._test_object_for_s3acl('DELETE', account) self.assertEqual(self._get_error_code(body), 'AccessDenied') @s3acl(s3acl_only=True) def test_object_DELETE_with_owner_permission(self): status, headers, body = self._test_object_for_s3acl('DELETE', 'test:tester') self.assertEqual(status.split()[0], '204') @s3acl(s3acl_only=True) def test_object_DELETE_with_write_permission(self): account = 'test:other' self._test_set_container_permission(account, 'WRITE') status, headers, body = self._test_object_for_s3acl('DELETE', account) self.assertEqual(status.split()[0], '204') @s3acl(s3acl_only=True) def test_object_DELETE_with_fullcontrol_permission(self): account = 'test:other' self._test_set_container_permission(account, 'FULL_CONTROL') status, headers, body = self._test_object_for_s3acl('DELETE', account) self.assertEqual(status.split()[0], '204') def _test_object_copy_for_s3acl(self, account, src_permission=None, src_path='/src_bucket/src_obj'): owner = 'test:tester' grants = [Grant(User(account), src_permission)] \ if src_permission else [Grant(User(owner), 'FULL_CONTROL')] src_o_headers = \ encode_acl('object', ACL(Owner(owner, owner), grants)) src_o_headers.update({'last-modified': self.last_modified}) self.swift.register( 'HEAD', join('/v1/AUTH_test', src_path.lstrip('/')), swob.HTTPOk, src_o_headers, None) req = Request.blank( '/bucket/object', environ={'REQUEST_METHOD': 'PUT'}, headers={'Authorization': 'AWS %s:hmac' % account, 'X-Amz-Copy-Source': src_path, 'Date': self.get_date_header()}) return self.call_s3api(req) @s3acl(s3acl_only=True) def test_object_PUT_copy_with_owner_permission(self): status, headers, body = \ self._test_object_copy_for_s3acl('test:tester') self.assertEqual(status.split()[0], '200') @s3acl(s3acl_only=True) def test_object_PUT_copy_with_fullcontrol_permission(self): status, headers, body = \ self._test_object_copy_for_s3acl('test:full_control', 'FULL_CONTROL') self.assertEqual(status.split()[0], '200') @s3acl(s3acl_only=True) def test_object_PUT_copy_with_grantee_permission(self): status, headers, body = \ self._test_object_copy_for_s3acl('test:write', 'READ') self.assertEqual(status.split()[0], '200') @s3acl(s3acl_only=True) def test_object_PUT_copy_without_src_obj_permission(self): status, headers, body = \ self._test_object_copy_for_s3acl('test:write') self.assertEqual(status.split()[0], '403') @s3acl(s3acl_only=True) def test_object_PUT_copy_without_dst_container_permission(self): status, headers, body = \ self._test_object_copy_for_s3acl('test:other', 'READ') self.assertEqual(status.split()[0], '403') @s3acl(s3acl_only=True) def test_object_PUT_copy_empty_src_path(self): self.swift.register('PUT', '/v1/AUTH_test/bucket/object', swob.HTTPPreconditionFailed, {}, None) status, headers, body = self._test_object_copy_for_s3acl( 'test:write', 'READ', src_path='') self.assertEqual(status.split()[0], '400') def test_cors_headers(self): # note: Access-Control-Allow-Methods would normally be expected in # response to an OPTIONS request but its included here in GET/PUT tests # to check that it is always passed back in S3Response cors_headers = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': ('GET, PUT, POST, COPY, ' 'DELETE, PUT, OPTIONS'), 'Access-Control-Expose-Headers': 'x-object-meta-test, x-object-meta-test=5funderscore, etag', } get_resp_headers = self.response_headers get_resp_headers['x-object-meta-test=5funderscore'] = 'underscored' self.swift.register( 'GET', '/v1/AUTH_test/bucket/cors-object', swob.HTTPOk, dict(get_resp_headers, **cors_headers), self.object_body) self.swift.register( 'PUT', '/v1/AUTH_test/bucket/cors-object', swob.HTTPCreated, dict({'etag': self.etag, 'last-modified': self.last_modified, 'x-object-meta-something': 'oh hai', 'x-object-meta-test=5funderscore': 'underscored'}, **cors_headers), None) req = Request.blank( '/bucket/cors-object', environ={'REQUEST_METHOD': 'GET'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header(), 'Origin': 'http://example.com', 'Access-Control-Request-Method': 'GET', 'Access-Control-Request-Headers': 'authorization'}) status, headers, body = self.call_s3api(req) self.assertEqual(status, '200 OK') self.assertIn('Access-Control-Allow-Origin', headers) self.assertEqual(headers['Access-Control-Allow-Origin'], '*') self.assertIn('Access-Control-Expose-Headers', headers) self.assertEqual( headers['Access-Control-Expose-Headers'], 'x-amz-meta-test, x-amz-meta-test_underscore, etag, ' 'x-amz-request-id, x-amz-id-2') self.assertIn('Access-Control-Allow-Methods', headers) self.assertEqual( headers['Access-Control-Allow-Methods'], 'GET, PUT, POST, DELETE, PUT, OPTIONS') self.assertIn('x-amz-meta-test_underscore', headers) self.assertEqual('underscored', headers['x-amz-meta-test_underscore']) req = Request.blank( '/bucket/cors-object', environ={'REQUEST_METHOD': 'PUT'}, headers={'Authorization': 'AWS test:tester:hmac', 'Date': self.get_date_header(), 'Origin': 'http://example.com', 'Access-Control-Request-Method': 'PUT', 'Access-Control-Request-Headers': 'authorization'}) status, headers, body = self.call_s3api(req) self.assertEqual(status, '200 OK') self.assertIn('Access-Control-Allow-Origin', headers) self.assertEqual(headers['Access-Control-Allow-Origin'], '*') self.assertIn('Access-Control-Expose-Headers', headers) self.assertEqual( headers['Access-Control-Expose-Headers'], 'x-amz-meta-test, x-amz-meta-test_underscore, etag, ' 'x-amz-request-id, x-amz-id-2') self.assertIn('Access-Control-Allow-Methods', headers) self.assertEqual( headers['Access-Control-Allow-Methods'], 'GET, PUT, POST, DELETE, PUT, OPTIONS') self.assertEqual('underscored', headers['x-amz-meta-test_underscore']) class TestS3ApiObjNonUTC(TestS3ApiObj): def setUp(self): self.orig_tz = os.environ.get('TZ', '') os.environ['TZ'] = 'EST+05EDT,M4.1.0,M10.5.0' time.tzset() super(TestS3ApiObjNonUTC, self).setUp() def tearDown(self): super(TestS3ApiObjNonUTC, self).tearDown() os.environ['TZ'] = self.orig_tz time.tzset() if __name__ == '__main__': unittest.main()