#2104 - S3 - Persist metadata for Presigned URL
This commit is contained in:
		
							parent
							
								
									920d074bb9
								
							
						
					
					
						commit
						b33c5dff06
					
				@ -1079,6 +1079,10 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
 | 
				
			|||||||
            if key:
 | 
					            if key:
 | 
				
			||||||
                if not key.acl.public_read and not signed_url:
 | 
					                if not key.acl.public_read and not signed_url:
 | 
				
			||||||
                    return 403, {}, ""
 | 
					                    return 403, {}, ""
 | 
				
			||||||
 | 
					            elif signed_url:
 | 
				
			||||||
 | 
					                # coming in from requests.get(s3.generate_presigned_url())
 | 
				
			||||||
 | 
					                if self._invalid_headers(request.url, dict(headers)):
 | 
				
			||||||
 | 
					                    return 403, {}, S3_INVALID_PRESIGNED_PARAMETERS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if hasattr(request, "body"):
 | 
					        if hasattr(request, "body"):
 | 
				
			||||||
            # Boto
 | 
					            # Boto
 | 
				
			||||||
@ -1287,6 +1291,7 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
            request.streaming = True
 | 
					            request.streaming = True
 | 
				
			||||||
            metadata = metadata_from_headers(request.headers)
 | 
					            metadata = metadata_from_headers(request.headers)
 | 
				
			||||||
 | 
					            metadata.update(metadata_from_headers(query))
 | 
				
			||||||
            new_key.set_metadata(metadata)
 | 
					            new_key.set_metadata(metadata)
 | 
				
			||||||
            new_key.set_acl(acl)
 | 
					            new_key.set_acl(acl)
 | 
				
			||||||
            new_key.website_redirect_location = request.headers.get(
 | 
					            new_key.website_redirect_location = request.headers.get(
 | 
				
			||||||
@ -1672,6 +1677,29 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
 | 
				
			|||||||
                "Method POST had only been implemented for multipart uploads and restore operations, so far"
 | 
					                "Method POST had only been implemented for multipart uploads and restore operations, so far"
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _invalid_headers(self, url, headers):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Verify whether the provided metadata in the URL is also present in the headers
 | 
				
			||||||
 | 
					        :param url: .../file.txt&content-type=app%2Fjson&Signature=..
 | 
				
			||||||
 | 
					        :param headers: Content-Type=app/json
 | 
				
			||||||
 | 
					        :return: True or False
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        metadata_to_check = {
 | 
				
			||||||
 | 
					            "content-disposition": "Content-Disposition",
 | 
				
			||||||
 | 
					            "content-encoding": "Content-Encoding",
 | 
				
			||||||
 | 
					            "content-language": "Content-Language",
 | 
				
			||||||
 | 
					            "content-length": "Content-Length",
 | 
				
			||||||
 | 
					            "content-md5": "Content-MD5",
 | 
				
			||||||
 | 
					            "content-type": "Content-Type",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        for url_key, header_key in metadata_to_check.items():
 | 
				
			||||||
 | 
					            metadata_in_url = re.search(url_key + "=(.+?)(&.+$|$)", url)
 | 
				
			||||||
 | 
					            if metadata_in_url:
 | 
				
			||||||
 | 
					                url_value = unquote(metadata_in_url.group(1))
 | 
				
			||||||
 | 
					                if header_key not in headers or (url_value != headers[header_key]):
 | 
				
			||||||
 | 
					                    return True
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
S3ResponseInstance = ResponseObject(s3_backend)
 | 
					S3ResponseInstance = ResponseObject(s3_backend)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -2214,6 +2242,15 @@ S3_ENCRYPTION_CONFIG = """<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			|||||||
</BucketEncryptionStatus>
 | 
					</BucketEncryptionStatus>
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					S3_INVALID_PRESIGNED_PARAMETERS = """<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<Error>
 | 
				
			||||||
 | 
					  <Code>SignatureDoesNotMatch</Code>
 | 
				
			||||||
 | 
					  <Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>
 | 
				
			||||||
 | 
					  <RequestId>0D68A23BB2E2215B</RequestId>
 | 
				
			||||||
 | 
					  <HostId>9Gjjt1m+cjU4OPvX9O9/8RuvnG41MRb/18Oux2o5H5MY7ISNTlXN+Dz9IG62/ILVxhAGI0qyPfg=</HostId>
 | 
				
			||||||
 | 
					</Error>
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
S3_NO_ENCRYPTION = """<?xml version="1.0" encoding="UTF-8"?>
 | 
					S3_NO_ENCRYPTION = """<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
<Error>
 | 
					<Error>
 | 
				
			||||||
  <Code>ServerSideEncryptionConfigurationNotFoundError</Code>
 | 
					  <Code>ServerSideEncryptionConfigurationNotFoundError</Code>
 | 
				
			||||||
 | 
				
			|||||||
@ -75,7 +75,11 @@ def metadata_from_headers(headers):
 | 
				
			|||||||
                # Check for special metadata that doesn't start with x-amz-meta
 | 
					                # Check for special metadata that doesn't start with x-amz-meta
 | 
				
			||||||
                meta_key = header
 | 
					                meta_key = header
 | 
				
			||||||
            if meta_key:
 | 
					            if meta_key:
 | 
				
			||||||
                metadata[meta_key] = headers[header]
 | 
					                metadata[meta_key] = (
 | 
				
			||||||
 | 
					                    headers[header][0]
 | 
				
			||||||
 | 
					                    if type(headers[header]) == list
 | 
				
			||||||
 | 
					                    else headers[header]
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
    return metadata
 | 
					    return metadata
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4583,3 +4583,104 @@ def test_encryption():
 | 
				
			|||||||
    conn.delete_bucket_encryption(Bucket="mybucket")
 | 
					    conn.delete_bucket_encryption(Bucket="mybucket")
 | 
				
			||||||
    with assert_raises(ClientError) as exc:
 | 
					    with assert_raises(ClientError) as exc:
 | 
				
			||||||
        conn.get_bucket_encryption(Bucket="mybucket")
 | 
					        conn.get_bucket_encryption(Bucket="mybucket")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@mock_s3
 | 
				
			||||||
 | 
					def test_presigned_url_restrict_parameters():
 | 
				
			||||||
 | 
					    # Only specific params can be set
 | 
				
			||||||
 | 
					    # Ensure error is thrown when adding custom metadata this way
 | 
				
			||||||
 | 
					    bucket = str(uuid.uuid4())
 | 
				
			||||||
 | 
					    key = "file.txt"
 | 
				
			||||||
 | 
					    conn = boto3.resource("s3", region_name="us-east-1")
 | 
				
			||||||
 | 
					    conn.create_bucket(Bucket=bucket)
 | 
				
			||||||
 | 
					    s3 = boto3.client("s3", region_name="us-east-1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Create a pre-signed url with some metadata.
 | 
				
			||||||
 | 
					    with assert_raises(botocore.exceptions.ParamValidationError) as err:
 | 
				
			||||||
 | 
					        s3.generate_presigned_url(
 | 
				
			||||||
 | 
					            ClientMethod="put_object",
 | 
				
			||||||
 | 
					            Params={"Bucket": bucket, "Key": key, "Unknown": "metadata"},
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    assert str(err.exception).should.equal(
 | 
				
			||||||
 | 
					        'Parameter validation failed:\nUnknown parameter in input: "Unknown", must be one of: ACL, Body, Bucket, CacheControl, ContentDisposition, ContentEncoding, ContentLanguage, ContentLength, ContentMD5, ContentType, Expires, GrantFullControl, GrantRead, GrantReadACP, GrantWriteACP, Key, Metadata, ServerSideEncryption, StorageClass, WebsiteRedirectLocation, SSECustomerAlgorithm, SSECustomerKey, SSECustomerKeyMD5, SSEKMSKeyId, SSEKMSEncryptionContext, RequestPayer, Tagging, ObjectLockMode, ObjectLockRetainUntilDate, ObjectLockLegalHoldStatus'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    s3.delete_bucket(Bucket=bucket)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@mock_s3
 | 
				
			||||||
 | 
					def test_presigned_put_url_with_approved_headers():
 | 
				
			||||||
 | 
					    bucket = str(uuid.uuid4())
 | 
				
			||||||
 | 
					    key = "file.txt"
 | 
				
			||||||
 | 
					    expected_contenttype = "app/sth"
 | 
				
			||||||
 | 
					    conn = boto3.resource("s3", region_name="us-east-1")
 | 
				
			||||||
 | 
					    conn.create_bucket(Bucket=bucket)
 | 
				
			||||||
 | 
					    s3 = boto3.client("s3", region_name="us-east-1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Create a pre-signed url with some metadata.
 | 
				
			||||||
 | 
					    url = s3.generate_presigned_url(
 | 
				
			||||||
 | 
					        ClientMethod="put_object",
 | 
				
			||||||
 | 
					        Params={"Bucket": bucket, "Key": key, "ContentType": expected_contenttype},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Verify S3 throws an error when the header is not provided
 | 
				
			||||||
 | 
					    response = requests.put(url, data="filecontent")
 | 
				
			||||||
 | 
					    response.status_code.should.equal(403)
 | 
				
			||||||
 | 
					    response.content.should.contain("<Code>SignatureDoesNotMatch</Code>")
 | 
				
			||||||
 | 
					    response.content.should.contain(
 | 
				
			||||||
 | 
					        "<Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Verify S3 throws an error when the header has the wrong value
 | 
				
			||||||
 | 
					    response = requests.put(
 | 
				
			||||||
 | 
					        url, data="filecontent", headers={"Content-Type": "application/unknown"}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    response.status_code.should.equal(403)
 | 
				
			||||||
 | 
					    response.content.should.contain("<Code>SignatureDoesNotMatch</Code>")
 | 
				
			||||||
 | 
					    response.content.should.contain(
 | 
				
			||||||
 | 
					        "<Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Verify S3 uploads correctly when providing the meta data
 | 
				
			||||||
 | 
					    response = requests.put(
 | 
				
			||||||
 | 
					        url, data="filecontent", headers={"Content-Type": expected_contenttype}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    response.status_code.should.equal(200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Assert the object exists
 | 
				
			||||||
 | 
					    obj = s3.get_object(Bucket=bucket, Key=key)
 | 
				
			||||||
 | 
					    obj["ContentType"].should.equal(expected_contenttype)
 | 
				
			||||||
 | 
					    obj["ContentLength"].should.equal(11)
 | 
				
			||||||
 | 
					    obj["Body"].read().should.equal("filecontent")
 | 
				
			||||||
 | 
					    obj["Metadata"].should.equal({})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    s3.delete_object(Bucket=bucket, Key=key)
 | 
				
			||||||
 | 
					    s3.delete_bucket(Bucket=bucket)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@mock_s3
 | 
				
			||||||
 | 
					def test_presigned_put_url_with_custom_headers():
 | 
				
			||||||
 | 
					    bucket = str(uuid.uuid4())
 | 
				
			||||||
 | 
					    key = "file.txt"
 | 
				
			||||||
 | 
					    conn = boto3.resource("s3", region_name="us-east-1")
 | 
				
			||||||
 | 
					    conn.create_bucket(Bucket=bucket)
 | 
				
			||||||
 | 
					    s3 = boto3.client("s3", region_name="us-east-1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Create a pre-signed url with some metadata.
 | 
				
			||||||
 | 
					    url = s3.generate_presigned_url(
 | 
				
			||||||
 | 
					        ClientMethod="put_object",
 | 
				
			||||||
 | 
					        Params={"Bucket": bucket, "Key": key, "Metadata": {"venue": "123"}},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Verify S3 uploads correctly when providing the meta data
 | 
				
			||||||
 | 
					    response = requests.put(url, data="filecontent")
 | 
				
			||||||
 | 
					    response.status_code.should.equal(200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Assert the object exists
 | 
				
			||||||
 | 
					    obj = s3.get_object(Bucket=bucket, Key=key)
 | 
				
			||||||
 | 
					    obj["ContentLength"].should.equal(11)
 | 
				
			||||||
 | 
					    obj["Body"].read().should.equal("filecontent")
 | 
				
			||||||
 | 
					    obj["Metadata"].should.equal({"venue": "123"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    s3.delete_object(Bucket=bucket, Key=key)
 | 
				
			||||||
 | 
					    s3.delete_bucket(Bucket=bucket)
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user