summaryrefslogtreecommitdiff
path: root/swift/common/middleware/s3api/s3request.py
diff options
context:
space:
mode:
Diffstat (limited to 'swift/common/middleware/s3api/s3request.py')
-rw-r--r--swift/common/middleware/s3api/s3request.py210
1 files changed, 160 insertions, 50 deletions
diff --git a/swift/common/middleware/s3api/s3request.py b/swift/common/middleware/s3api/s3request.py
index 5833921f8..86dd6f75f 100644
--- a/swift/common/middleware/s3api/s3request.py
+++ b/swift/common/middleware/s3api/s3request.py
@@ -14,7 +14,7 @@
# limitations under the License.
import base64
-from collections import defaultdict
+from collections import defaultdict, OrderedDict
from email.header import Header
from hashlib import sha1, sha256, md5
import hmac
@@ -24,7 +24,7 @@ import six
from six.moves.urllib.parse import quote, unquote, parse_qsl
import string
-from swift.common.utils import split_path
+from swift.common.utils import split_path, json, get_swift_info
from swift.common import swob
from swift.common.http import HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, \
HTTP_NO_CONTENT, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, \
@@ -44,7 +44,7 @@ from swift.common.middleware.s3api.controllers import ServiceController, \
UploadController, UploadsController, VersioningController, \
UnsupportedController, S3AclController, BucketController
from swift.common.middleware.s3api.s3response import AccessDenied, \
- InvalidArgument, InvalidDigest, \
+ InvalidArgument, InvalidDigest, BucketAlreadyOwnedByYou, \
RequestTimeTooSkewed, S3Response, SignatureDoesNotMatch, \
BucketAlreadyExists, BucketNotEmpty, EntityTooLarge, \
InternalError, NoSuchBucket, NoSuchKey, PreconditionFailed, InvalidRange, \
@@ -116,9 +116,10 @@ class SigV4Mixin(object):
"""
def check_signature(self, secret):
+ secret = utf8encode(secret)
user_signature = self.signature
derived_secret = 'AWS4' + secret
- for scope_piece in self.scope:
+ for scope_piece in self.scope.values():
derived_secret = hmac.new(
derived_secret, scope_piece, sha256).digest()
valid_signature = hmac.new(
@@ -176,6 +177,8 @@ class SigV4Mixin(object):
err = None
try:
expires = int(self.params['X-Amz-Expires'])
+ except KeyError:
+ raise AccessDenied()
except ValueError:
err = 'X-Amz-Expires should be a number'
else:
@@ -193,6 +196,15 @@ class SigV4Mixin(object):
if int(self.timestamp) + expires < S3Timestamp.now():
raise AccessDenied('Request has expired')
+ def _parse_credential(self, credential_string):
+ parts = credential_string.split("/")
+ # credential must be in following format:
+ # <access-key-id>/<date>/<AWS-region>/<AWS-service>/aws4_request
+ if not parts[0] or len(parts) != 5:
+ raise AccessDenied()
+ return dict(zip(['access', 'date', 'region', 'service', 'terminal'],
+ parts))
+
def _parse_query_authentication(self):
"""
Parse v4 query authentication
@@ -205,10 +217,11 @@ class SigV4Mixin(object):
raise InvalidArgument('X-Amz-Algorithm',
self.params.get('X-Amz-Algorithm'))
try:
- cred_param = self.params['X-Amz-Credential'].split("/")
- access = cred_param[0]
+ cred_param = self._parse_credential(
+ self.params['X-Amz-Credential'])
sig = self.params['X-Amz-Signature']
- expires = self.params['X-Amz-Expires']
+ if not sig:
+ raise AccessDenied()
except KeyError:
raise AccessDenied()
@@ -220,12 +233,26 @@ class SigV4Mixin(object):
self._signed_headers = set(signed_headers.split(';'))
- # credential must be in following format:
- # <access-key-id>/<date>/<AWS-region>/<AWS-service>/aws4_request
- if not all([access, sig, len(cred_param) == 5, expires]):
- raise AccessDenied()
+ invalid_messages = {
+ 'date': 'Invalid credential date "%s". This date is not the same '
+ 'as X-Amz-Date: "%s".',
+ 'region': "Error parsing the X-Amz-Credential parameter; "
+ "the region '%s' is wrong; expecting '%s'",
+ 'service': 'Error parsing the X-Amz-Credential parameter; '
+ 'incorrect service "%s". This endpoint belongs to "%s".',
+ 'terminal': 'Error parsing the X-Amz-Credential parameter; '
+ 'incorrect terminal "%s". This endpoint uses "%s".',
+ }
+ for key in ('date', 'region', 'service', 'terminal'):
+ if cred_param[key] != self.scope[key]:
+ kwargs = {}
+ if key == 'region':
+ kwargs = {'region': self.scope['region']}
+ raise AuthorizationQueryParametersError(
+ invalid_messages[key] % (cred_param[key], self.scope[key]),
+ **kwargs)
- return access, sig
+ return cred_param['access'], sig
def _parse_header_authentication(self):
"""
@@ -237,23 +264,39 @@ class SigV4Mixin(object):
"""
auth_str = self.headers['Authorization']
- cred_param = auth_str.partition(
- "Credential=")[2].split(',')[0].split("/")
- access = cred_param[0]
+ cred_param = self._parse_credential(auth_str.partition(
+ "Credential=")[2].split(',')[0])
sig = auth_str.partition("Signature=")[2].split(',')[0]
+ if not sig:
+ raise AccessDenied()
signed_headers = auth_str.partition(
"SignedHeaders=")[2].split(',', 1)[0]
- # credential must be in following format:
- # <access-key-id>/<date>/<AWS-region>/<AWS-service>/aws4_request
- if not all([access, sig, len(cred_param) == 5]):
- raise AccessDenied()
if not signed_headers:
# TODO: make sure if is it Malformed?
raise AuthorizationHeaderMalformed()
+ invalid_messages = {
+ 'date': 'Invalid credential date "%s". This date is not the same '
+ 'as X-Amz-Date: "%s".',
+ 'region': "The authorization header is malformed; the region '%s' "
+ "is wrong; expecting '%s'",
+ 'service': 'The authorization header is malformed; incorrect '
+ 'service "%s". This endpoint belongs to "%s".',
+ 'terminal': 'The authorization header is malformed; incorrect '
+ 'terminal "%s". This endpoint uses "%s".',
+ }
+ for key in ('date', 'region', 'service', 'terminal'):
+ if cred_param[key] != self.scope[key]:
+ kwargs = {}
+ if key == 'region':
+ kwargs = {'region': self.scope['region']}
+ raise AuthorizationHeaderMalformed(
+ invalid_messages[key] % (cred_param[key], self.scope[key]),
+ **kwargs)
+
self._signed_headers = set(signed_headers.split(';'))
- return access, sig
+ return cred_param['access'], sig
def _canonical_query_string(self):
return '&'.join(
@@ -363,8 +406,12 @@ class SigV4Mixin(object):
@property
def scope(self):
- return [self.timestamp.amz_date_format.split('T')[0],
- self.location, SERVICE, 'aws4_request']
+ return OrderedDict([
+ ('date', self.timestamp.amz_date_format.split('T')[0]),
+ ('region', self.location),
+ ('service', SERVICE),
+ ('terminal', 'aws4_request'),
+ ])
def _string_to_sign(self):
"""
@@ -372,9 +419,19 @@ class SigV4Mixin(object):
"""
return '\n'.join(['AWS4-HMAC-SHA256',
self.timestamp.amz_date_format,
- '/'.join(self.scope),
+ '/'.join(self.scope.values()),
sha256(self._canonical_request()).hexdigest()])
+ def signature_does_not_match_kwargs(self):
+ kwargs = super(SigV4Mixin, self).signature_does_not_match_kwargs()
+ cr = self._canonical_request()
+ kwargs.update({
+ 'canonical_request': cr,
+ 'canonical_request_bytes': ' '.join(
+ format(ord(c), '02x') for c in cr),
+ })
+ return kwargs
+
def get_request_class(env, s3_acl):
"""
@@ -404,8 +461,8 @@ class S3Request(swob.Request):
bucket_acl = _header_acl_property('container')
object_acl = _header_acl_property('object')
- def __init__(self, env, app=None, slo_enabled=True,
- storage_domain='', location='US', force_request_log=False,
+ def __init__(self, env, app=None, slo_enabled=True, storage_domain='',
+ location='us-east-1', force_request_log=False,
dns_compliant_bucket_names=True, allow_multipart_uploads=True,
allow_no_owner=False):
# NOTE: app and allow_no_owner are not used by this class, need for
@@ -434,20 +491,12 @@ class S3Request(swob.Request):
self.user_id = None
self.slo_enabled = slo_enabled
- # NOTE(andrey-mp): substitute authorization header for next modules
- # in pipeline (s3token). it uses this and X-Auth-Token in specific
- # format.
- # (kota_): yeah, the reason we need this is s3token only supports
- # v2 like header consists of AWS access:signature. Since the commit
- # b626a3ca86e467fc7564eac236b9ee2efd49bdcc, the s3token is in swift3
- # repo so probably we need to change s3token to support v4 format.
- self.headers['Authorization'] = 'AWS %s:%s' % (
- self.access_key, self.signature)
# Avoids that swift.swob.Response replaces Location header value
# by full URL when absolute path given. See swift.swob for more detail.
self.environ['swift.leave_relative_location'] = True
def check_signature(self, secret):
+ secret = utf8encode(secret)
user_signature = self.signature
valid_signature = base64.b64encode(hmac.new(
secret, self.string_to_sign, sha1).digest()).strip()
@@ -577,8 +626,10 @@ class S3Request(swob.Request):
:raises: NotS3Request
"""
if self._is_query_auth:
+ self._validate_expire_param()
return self._parse_query_authentication()
elif self._is_header_auth:
+ self._validate_dates()
return self._parse_header_authentication()
else:
# if this request is neither query auth nor header auth
@@ -593,7 +644,7 @@ class S3Request(swob.Request):
# Expires header is a float since epoch
try:
ex = S3Timestamp(float(self.params['Expires']))
- except ValueError:
+ except (KeyError, ValueError):
raise AccessDenied()
if S3Timestamp.now() > ex:
@@ -610,11 +661,6 @@ class S3Request(swob.Request):
:raises: AccessDenied
:raises: RequestTimeTooSkewed
"""
- if self._is_query_auth:
- self._validate_expire_param()
- # TODO: make sure the case if timestamp param in query
- return
-
date_header = self.headers.get('Date')
amz_date_header = self.headers.get('X-Amz-Date')
if not date_header and not amz_date_header:
@@ -642,8 +688,6 @@ class S3Request(swob.Request):
raise InvalidArgument('Content-Length',
self.environ['CONTENT_LENGTH'])
- self._validate_dates()
-
value = _header_strip(self.headers.get('Content-MD5'))
if value is not None:
if not re.match('^[A-Za-z0-9+/]+={0,2}$', value):
@@ -688,12 +732,33 @@ class S3Request(swob.Request):
if 'x-amz-mfa' in self.headers:
raise S3NotImplemented('MFA Delete is not supported.')
- if 'x-amz-server-side-encryption' in self.headers:
- raise S3NotImplemented('Server-side encryption is not supported.')
+ sse_value = self.headers.get('x-amz-server-side-encryption')
+ if sse_value is not None:
+ if sse_value not in ('aws:kms', 'AES256'):
+ raise InvalidArgument(
+ 'x-amz-server-side-encryption', sse_value,
+ 'The encryption method specified is not supported')
+ encryption_enabled = get_swift_info(admin=True)['admin'].get(
+ 'encryption', {}).get('enabled')
+ if not encryption_enabled or sse_value != 'AES256':
+ raise S3NotImplemented(
+ 'Server-side encryption is not supported.')
if 'x-amz-website-redirect-location' in self.headers:
raise S3NotImplemented('Website redirection is not supported.')
+ # https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
+ # describes some of what would be required to support this
+ if any(['aws-chunked' in self.headers.get('content-encoding', ''),
+ 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD' == self.headers.get(
+ 'x-amz-content-sha256', ''),
+ 'x-amz-decoded-content-length' in self.headers]):
+ raise S3NotImplemented('Transfering payloads in multiple chunks '
+ 'using aws-chunked is not supported.')
+
+ if 'x-amz-tagging' in self.headers:
+ raise S3NotImplemented('Object tagging is not supported.')
+
@property
def body(self):
"""
@@ -866,6 +931,15 @@ class S3Request(swob.Request):
buf.append(path)
return '\n'.join(buf)
+ def signature_does_not_match_kwargs(self):
+ return {
+ 'a_w_s_access_key_id': self.access_key,
+ 'string_to_sign': self.string_to_sign,
+ 'signature_provided': self.signature,
+ 'string_to_sign_bytes': ' '.join(
+ format(ord(c), '02x') for c in self.string_to_sign),
+ }
+
@property
def controller_name(self):
return self.controller.__name__[:-len('Controller')]
@@ -982,6 +1056,22 @@ class S3Request(swob.Request):
env['HTTP_X_COPY_FROM'] = env['HTTP_X_AMZ_COPY_SOURCE']
del env['HTTP_X_AMZ_COPY_SOURCE']
env['CONTENT_LENGTH'] = '0'
+ # Content type cannot be modified on COPY
+ env.pop('CONTENT_TYPE', None)
+ if env.pop('HTTP_X_AMZ_METADATA_DIRECTIVE', None) == 'REPLACE':
+ env['HTTP_X_FRESH_METADATA'] = 'True'
+ else:
+ copy_exclude_headers = ('HTTP_CONTENT_DISPOSITION',
+ 'HTTP_CONTENT_ENCODING',
+ 'HTTP_CONTENT_LANGUAGE',
+ 'HTTP_EXPIRES',
+ 'HTTP_CACHE_CONTROL',
+ 'HTTP_X_ROBOTS_TAG')
+ for key in copy_exclude_headers:
+ env.pop(key, None)
+ for key in list(env.keys()):
+ if key.startswith('HTTP_X_OBJECT_META_'):
+ del env[key]
if self.force_request_log:
env['swift.proxy_access_log_made'] = False
@@ -1059,6 +1149,7 @@ class S3Request(swob.Request):
],
'PUT': [
HTTP_CREATED,
+ HTTP_ACCEPTED, # For SLO with heartbeating
],
'POST': [
HTTP_ACCEPTED,
@@ -1071,6 +1162,20 @@ class S3Request(swob.Request):
return code_map[method]
+ def _bucket_put_accepted_error(self, container, app):
+ sw_req = self.to_swift_req('HEAD', container, None)
+ info = get_container_info(sw_req.environ, app)
+ sysmeta = info.get('sysmeta', {})
+ try:
+ acl = json.loads(sysmeta.get('s3api-acl',
+ sysmeta.get('swift3-acl', '{}')))
+ owner = acl.get('Owner')
+ except (ValueError, TypeError, KeyError):
+ owner = None
+ if owner is None or owner == self.user_id:
+ raise BucketAlreadyOwnedByYou(container)
+ raise BucketAlreadyExists(container)
+
def _swift_error_codes(self, method, container, obj, env, app):
"""
Returns a dict from expected Swift error codes to the corresponding S3
@@ -1092,7 +1197,8 @@ class S3Request(swob.Request):
HTTP_NOT_FOUND: (NoSuchBucket, container),
},
'PUT': {
- HTTP_ACCEPTED: (BucketAlreadyExists, container),
+ HTTP_ACCEPTED: (self._bucket_put_accepted_error, container,
+ app),
},
'POST': {
HTTP_NOT_FOUND: (NoSuchBucket, container),
@@ -1202,7 +1308,8 @@ class S3Request(swob.Request):
if status == HTTP_BAD_REQUEST:
raise BadSwiftRequest(err_msg)
if status == HTTP_UNAUTHORIZED:
- raise SignatureDoesNotMatch()
+ raise SignatureDoesNotMatch(
+ **self.signature_does_not_match_kwargs())
if status == HTTP_FORBIDDEN:
raise AccessDenied()
@@ -1294,8 +1401,8 @@ class S3AclRequest(S3Request):
"""
S3Acl request object.
"""
- def __init__(self, env, app, slo_enabled=True,
- storage_domain='', location='US', force_request_log=False,
+ def __init__(self, env, app, slo_enabled=True, storage_domain='',
+ location='us-east-1', force_request_log=False,
dns_compliant_bucket_names=True, allow_multipart_uploads=True,
allow_no_owner=False):
super(S3AclRequest, self).__init__(
@@ -1325,7 +1432,8 @@ class S3AclRequest(S3Request):
sw_resp = sw_req.get_response(app)
if not sw_req.remote_user:
- raise SignatureDoesNotMatch()
+ raise SignatureDoesNotMatch(
+ **self.signature_does_not_match_kwargs())
_, self.account, _ = split_path(sw_resp.environ['PATH_INFO'],
2, 3, True)
@@ -1341,9 +1449,11 @@ class S3AclRequest(S3Request):
# tempauth
self.user_id = self.access_key
+ sw_req.environ.get('swift.authorize', lambda req: None)(sw_req)
+ self.environ['swift_owner'] = sw_req.environ.get('swift_owner', False)
+
# Need to skip S3 authorization on subsequent requests to prevent
# overwriting the account in PATH_INFO
- del self.headers['Authorization']
del self.environ['s3api.auth_details']
def to_swift_req(self, method, container, obj, query=None,