s3api: Add basic GET object-lock support
Some tooling out there, like Ansible, will always call to see if object-lock is enabled on a bucket/container. This fails as Swift doesn't understand the object-lock or the get object lock api[0]. When you use the get-object-lock-configuration to a bucket in s3 that doesn't have it applied it returns a specific 404: GET /?object-lock HTTP/1.1" 404 None ... <?xml version="1.0" encoding="UTF-8"?> <Error> <Code>ObjectLockConfigurationNotFoundError</Code> <Message>Object Lock configuration does not exist for this bucket</Message> <BucketName>bucket_name</BucketName> <RequestId>83VQBYP0SENV3VP4</RequestId> </Error>' This patch doesn't add support for get_object lock, instead it always returns a similar 404 as supplied by s3, so clients know it's not enabled. Also add a object-lock PUT 501 response. [0] https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectLockConfiguration.html Change-Id: Icff8cf57474dfad975a4f45bf2d500c2682c1129
This commit is contained in:
parent
5555980fb5
commit
0996433fe5
@ -150,7 +150,6 @@ ceph_s3:
|
|||||||
s3tests_boto3.functional.test_s3.test_object_lock_delete_object_with_retention: {status: KNOWN}
|
s3tests_boto3.functional.test_s3.test_object_lock_delete_object_with_retention: {status: KNOWN}
|
||||||
s3tests_boto3.functional.test_s3.test_object_lock_get_legal_hold_invalid_bucket: {status: KNOWN}
|
s3tests_boto3.functional.test_s3.test_object_lock_get_legal_hold_invalid_bucket: {status: KNOWN}
|
||||||
s3tests_boto3.functional.test_s3.test_object_lock_get_obj_lock: {status: KNOWN}
|
s3tests_boto3.functional.test_s3.test_object_lock_get_obj_lock: {status: KNOWN}
|
||||||
s3tests_boto3.functional.test_s3.test_object_lock_get_obj_lock_invalid_bucket: {status: KNOWN}
|
|
||||||
s3tests_boto3.functional.test_s3.test_object_lock_get_obj_metadata: {status: KNOWN}
|
s3tests_boto3.functional.test_s3.test_object_lock_get_obj_metadata: {status: KNOWN}
|
||||||
s3tests_boto3.functional.test_s3.test_object_lock_get_obj_retention: {status: KNOWN}
|
s3tests_boto3.functional.test_s3.test_object_lock_get_obj_retention: {status: KNOWN}
|
||||||
s3tests_boto3.functional.test_s3.test_object_lock_get_obj_retention_invalid_bucket: {status: KNOWN}
|
s3tests_boto3.functional.test_s3.test_object_lock_get_obj_retention_invalid_bucket: {status: KNOWN}
|
||||||
@ -159,6 +158,9 @@ ceph_s3:
|
|||||||
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_lock: {status: KNOWN}
|
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_lock: {status: KNOWN}
|
||||||
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_lock_invalid_bucket: {status: KNOWN}
|
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_lock_invalid_bucket: {status: KNOWN}
|
||||||
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_lock_invalid_days: {status: KNOWN}
|
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_lock_invalid_days: {status: KNOWN}
|
||||||
|
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_lock_invalid_status {status: KNOWN}
|
||||||
|
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_lock_invalid_years {status: KNOWN}
|
||||||
|
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_lock_with_days_and_years {status: KNOWN}
|
||||||
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_retention: {status: KNOWN}
|
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_retention: {status: KNOWN}
|
||||||
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_retention_increase_period: {status: KNOWN}
|
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_retention_increase_period: {status: KNOWN}
|
||||||
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_retention_invalid_bucket: {status: KNOWN}
|
s3tests_boto3.functional.test_s3.test_object_lock_put_obj_retention_invalid_bucket: {status: KNOWN}
|
||||||
|
@ -33,6 +33,8 @@ from swift.common.middleware.s3api.controllers.versioning import \
|
|||||||
VersioningController
|
VersioningController
|
||||||
from swift.common.middleware.s3api.controllers.tagging import \
|
from swift.common.middleware.s3api.controllers.tagging import \
|
||||||
TaggingController
|
TaggingController
|
||||||
|
from swift.common.middleware.s3api.controllers.object_lock import \
|
||||||
|
ObjectLockController
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Controller',
|
'Controller',
|
||||||
@ -50,6 +52,7 @@ __all__ = [
|
|||||||
'LoggingStatusController',
|
'LoggingStatusController',
|
||||||
'VersioningController',
|
'VersioningController',
|
||||||
'TaggingController',
|
'TaggingController',
|
||||||
|
'ObjectLockController',
|
||||||
|
|
||||||
'UnsupportedController',
|
'UnsupportedController',
|
||||||
]
|
]
|
||||||
|
44
swift/common/middleware/s3api/controllers/object_lock.py
Normal file
44
swift/common/middleware/s3api/controllers/object_lock.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Copyright (c) 2010-2023 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.
|
||||||
|
|
||||||
|
from swift.common.utils import public
|
||||||
|
|
||||||
|
from swift.common.middleware.s3api.controllers.base import Controller, \
|
||||||
|
bucket_operation, S3NotImplemented
|
||||||
|
from swift.common.middleware.s3api.s3response import \
|
||||||
|
ObjectLockConfigurationNotFoundError
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectLockController(Controller):
|
||||||
|
"""
|
||||||
|
Handles GET object-lock request, which always returns
|
||||||
|
<ObjectLockEnabled>Disabled</ObjectLockEnabled>
|
||||||
|
"""
|
||||||
|
@public
|
||||||
|
@bucket_operation
|
||||||
|
def GET(self, req):
|
||||||
|
"""
|
||||||
|
Handles GET object-lock param calls.
|
||||||
|
"""
|
||||||
|
raise ObjectLockConfigurationNotFoundError(req.container_name)
|
||||||
|
|
||||||
|
@public
|
||||||
|
@bucket_operation
|
||||||
|
def PUT(self, req):
|
||||||
|
"""
|
||||||
|
Handles PUT object-lock param calls.
|
||||||
|
"""
|
||||||
|
# Basically we don't support it, so return a 501
|
||||||
|
raise S3NotImplemented('The requested resource is not implemented')
|
@ -47,7 +47,7 @@ from swift.common.middleware.s3api.controllers import ServiceController, \
|
|||||||
LocationController, LoggingStatusController, PartController, \
|
LocationController, LoggingStatusController, PartController, \
|
||||||
UploadController, UploadsController, VersioningController, \
|
UploadController, UploadsController, VersioningController, \
|
||||||
UnsupportedController, S3AclController, BucketController, \
|
UnsupportedController, S3AclController, BucketController, \
|
||||||
TaggingController
|
TaggingController, ObjectLockController
|
||||||
from swift.common.middleware.s3api.s3response import AccessDenied, \
|
from swift.common.middleware.s3api.s3response import AccessDenied, \
|
||||||
InvalidArgument, InvalidDigest, BucketAlreadyOwnedByYou, \
|
InvalidArgument, InvalidDigest, BucketAlreadyOwnedByYou, \
|
||||||
RequestTimeTooSkewed, S3Response, SignatureDoesNotMatch, \
|
RequestTimeTooSkewed, S3Response, SignatureDoesNotMatch, \
|
||||||
@ -74,7 +74,8 @@ ALLOWED_SUB_RESOURCES = sorted([
|
|||||||
'versionId', 'versioning', 'versions', 'website',
|
'versionId', 'versioning', 'versions', 'website',
|
||||||
'response-cache-control', 'response-content-disposition',
|
'response-cache-control', 'response-content-disposition',
|
||||||
'response-content-encoding', 'response-content-language',
|
'response-content-encoding', 'response-content-language',
|
||||||
'response-content-type', 'response-expires', 'cors', 'tagging', 'restore'
|
'response-content-type', 'response-expires', 'cors', 'tagging', 'restore',
|
||||||
|
'object-lock'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@ -103,6 +104,7 @@ def _header_acl_property(resource):
|
|||||||
"""
|
"""
|
||||||
Set and retrieve the acl in self.headers
|
Set and retrieve the acl in self.headers
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def getter(self):
|
def getter(self):
|
||||||
return getattr(self, '_%s' % resource)
|
return getattr(self, '_%s' % resource)
|
||||||
|
|
||||||
@ -121,6 +123,7 @@ class HashingInput(object):
|
|||||||
"""
|
"""
|
||||||
wsgi.input wrapper to verify the hash of the input as it's read.
|
wsgi.input wrapper to verify the hash of the input as it's read.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, reader, content_length, hasher, expected_hex_hash):
|
def __init__(self, reader, content_length, hasher, expected_hex_hash):
|
||||||
self._input = reader
|
self._input = reader
|
||||||
self._to_read = content_length
|
self._to_read = content_length
|
||||||
@ -1051,6 +1054,8 @@ class S3Request(swob.Request):
|
|||||||
return VersioningController
|
return VersioningController
|
||||||
if 'tagging' in self.params:
|
if 'tagging' in self.params:
|
||||||
return TaggingController
|
return TaggingController
|
||||||
|
if 'object-lock' in self.params:
|
||||||
|
return ObjectLockController
|
||||||
|
|
||||||
unsupported = ('notification', 'policy', 'requestPayment', 'torrent',
|
unsupported = ('notification', 'policy', 'requestPayment', 'torrent',
|
||||||
'website', 'cors', 'restore')
|
'website', 'cors', 'restore')
|
||||||
@ -1536,6 +1541,7 @@ class S3AclRequest(S3Request):
|
|||||||
"""
|
"""
|
||||||
S3Acl request object.
|
S3Acl request object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, env, app=None, conf=None):
|
def __init__(self, env, app=None, conf=None):
|
||||||
super(S3AclRequest, self).__init__(env, app, conf)
|
super(S3AclRequest, self).__init__(env, app, conf)
|
||||||
self.authenticate(app)
|
self.authenticate(app)
|
||||||
|
@ -111,6 +111,7 @@ class S3Response(S3ResponseBase, swob.Response):
|
|||||||
headers instead of Swift's HeaderKeyDict. This also translates Swift
|
headers instead of Swift's HeaderKeyDict. This also translates Swift
|
||||||
specific headers to S3 headers.
|
specific headers to S3 headers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
swob.Response.__init__(self, *args, **kwargs)
|
swob.Response.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
@ -613,6 +614,16 @@ class NoSuchKey(ErrorResponse):
|
|||||||
ErrorResponse.__init__(self, msg, key=key, *args, **kwargs)
|
ErrorResponse.__init__(self, msg, key=key, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectLockConfigurationNotFoundError(ErrorResponse):
|
||||||
|
_status = '404 Not found'
|
||||||
|
_msg = 'Object Lock configuration does not exist for this bucket'
|
||||||
|
|
||||||
|
def __init__(self, bucket, msg=None, *args, **kwargs):
|
||||||
|
if not bucket:
|
||||||
|
raise InternalError()
|
||||||
|
ErrorResponse.__init__(self, msg, bucket_name=bucket, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class NoSuchLifecycleConfiguration(ErrorResponse):
|
class NoSuchLifecycleConfiguration(ErrorResponse):
|
||||||
_status = '404 Not Found'
|
_status = '404 Not Found'
|
||||||
_msg = 'The lifecycle configuration does not exist. .'
|
_msg = 'The lifecycle configuration does not exist. .'
|
||||||
|
@ -567,6 +567,58 @@ class TestS3ApiBucket(S3ApiBaseBoto3):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
ctx.exception.response['Error']['Code'], 'MethodNotAllowed')
|
ctx.exception.response['Error']['Code'], 'MethodNotAllowed')
|
||||||
|
|
||||||
|
def test_bucket_get_object_lock_configuration(self):
|
||||||
|
bucket = 'bucket'
|
||||||
|
|
||||||
|
# PUT Bucket
|
||||||
|
resp = self.conn.create_bucket(Bucket=bucket)
|
||||||
|
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||||
|
headers = resp['ResponseMetadata']['HTTPHeaders']
|
||||||
|
self.assertCommonResponseHeaders(headers)
|
||||||
|
|
||||||
|
# now attempt to get object_lock_configuration from new bucket.
|
||||||
|
with self.assertRaises(botocore.exceptions.ClientError) as ce:
|
||||||
|
self.conn.get_object_lock_configuration(
|
||||||
|
Bucket=bucket)
|
||||||
|
self.assertEqual(
|
||||||
|
ce.exception.response['ResponseMetadata']['HTTPStatusCode'],
|
||||||
|
404)
|
||||||
|
self.assertEqual(
|
||||||
|
ce.exception.response['Error']['Code'],
|
||||||
|
'ObjectLockConfigurationNotFoundError')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
str(ce.exception),
|
||||||
|
'An error occurred (ObjectLockConfigurationNotFoundError) when '
|
||||||
|
'calling the GetObjectLockConfiguration operation: Object Lock '
|
||||||
|
'configuration does not exist for this bucket')
|
||||||
|
|
||||||
|
def test_bucket_put_object_lock_configuration(self):
|
||||||
|
bucket = 'bucket'
|
||||||
|
|
||||||
|
# PUT Bucket
|
||||||
|
resp = self.conn.create_bucket(Bucket=bucket)
|
||||||
|
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||||
|
headers = resp['ResponseMetadata']['HTTPHeaders']
|
||||||
|
self.assertCommonResponseHeaders(headers)
|
||||||
|
|
||||||
|
# now attempt to get object_lock_configuration from new bucket.
|
||||||
|
with self.assertRaises(botocore.exceptions.ClientError) as ce:
|
||||||
|
self.conn.put_object_lock_configuration(
|
||||||
|
Bucket=bucket, ObjectLockConfiguration={})
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
ce.exception.response['ResponseMetadata']['HTTPStatusCode'],
|
||||||
|
501)
|
||||||
|
self.assertEqual(
|
||||||
|
ce.exception.response['Error']['Code'],
|
||||||
|
'NotImplemented')
|
||||||
|
|
||||||
|
self.assertEqual(str(ce.exception),
|
||||||
|
'An error occurred (NotImplemented) when calling '
|
||||||
|
'the PutObjectLockConfiguration operation: The '
|
||||||
|
'requested resource is not implemented')
|
||||||
|
|
||||||
|
|
||||||
class TestS3ApiBucketSigV4(TestS3ApiBucket):
|
class TestS3ApiBucketSigV4(TestS3ApiBucket):
|
||||||
@classmethod
|
@classmethod
|
||||||
|
51
test/s3api/test_object_lock.py
Normal file
51
test/s3api/test_object_lock.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Copyright (c) 2010-2023 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 botocore
|
||||||
|
|
||||||
|
from test.s3api import BaseS3TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestObjectLockConfiguration(BaseS3TestCase):
|
||||||
|
|
||||||
|
maxDiff = None
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = self.get_s3_client(1)
|
||||||
|
self.bucket_name = self.create_name('objlock')
|
||||||
|
resp = self.client.create_bucket(Bucket=self.bucket_name)
|
||||||
|
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.clear_bucket(self.client, self.bucket_name)
|
||||||
|
super(TestObjectLockConfiguration, self).tearDown()
|
||||||
|
|
||||||
|
def test_get_object_lock_configuration(self):
|
||||||
|
with self.assertRaises(botocore.exceptions.ClientError) as ce:
|
||||||
|
self.client.get_object_lock_configuration(
|
||||||
|
Bucket=self.bucket_name)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
ce.exception.response['ResponseMetadata']['HTTPStatusCode'],
|
||||||
|
404)
|
||||||
|
self.assertEqual(
|
||||||
|
ce.exception.response['Error']['Code'],
|
||||||
|
'ObjectLockConfigurationNotFoundError')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
str(ce.exception),
|
||||||
|
'An error occurred (ObjectLockConfigurationNotFoundError) when '
|
||||||
|
'calling the GetObjectLockConfiguration operation: Object Lock '
|
||||||
|
'configuration does not exist for this bucket')
|
67
test/unit/common/middleware/s3api/test_object_lock.py
Normal file
67
test/unit/common/middleware/s3api/test_object_lock.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Copyright (c) 2010-2023 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 unittest
|
||||||
|
|
||||||
|
from swift.common.swob import Request
|
||||||
|
|
||||||
|
from test.unit.common.middleware.s3api import S3ApiTestCase
|
||||||
|
from swift.common.middleware.s3api.etree import fromstring
|
||||||
|
|
||||||
|
|
||||||
|
class TestS3ApiObjectLock(S3ApiTestCase):
|
||||||
|
|
||||||
|
# The object-lock controller currently only returns a static response
|
||||||
|
# as disabled. Things like ansible need this. So there isn't much to test.
|
||||||
|
|
||||||
|
def test_get_object_lock(self):
|
||||||
|
req = Request.blank('/bucket?object-lock',
|
||||||
|
environ={'REQUEST_METHOD': 'GET',
|
||||||
|
'swift.trans_id': 'txt1234'},
|
||||||
|
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.assertTrue(elem.getchildren())
|
||||||
|
for child in elem.iterchildren():
|
||||||
|
if child.tag == 'Code':
|
||||||
|
self.assertEqual(child.text,
|
||||||
|
'ObjectLockConfigurationNotFoundError')
|
||||||
|
elif child.tag == 'Message':
|
||||||
|
self.assertEqual(child.text,
|
||||||
|
'Object Lock configuration does not exist '
|
||||||
|
'for this bucket')
|
||||||
|
elif child.tag == 'RequestId':
|
||||||
|
self.assertEqual(child.text,
|
||||||
|
'txt1234')
|
||||||
|
elif child.tag == 'BucketName':
|
||||||
|
self.assertEqual(child.text, 'bucket')
|
||||||
|
else:
|
||||||
|
self.fail('Found unknown sub entry')
|
||||||
|
|
||||||
|
def test_put_object_lock(self):
|
||||||
|
req = Request.blank('/bucket?object-lock',
|
||||||
|
environ={'REQUEST_METHOD': 'PUT'},
|
||||||
|
headers={'Authorization': 'AWS test:tester:hmac',
|
||||||
|
'Date': self.get_date_header()})
|
||||||
|
status, headers, body = self.call_s3api(req)
|
||||||
|
# This currently isn't implemented.
|
||||||
|
self.assertEqual(status.split()[0], '501')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
Loading…
Reference in New Issue
Block a user