Skip to content

Commit

Permalink
get_presigned_post now async (#801)
Browse files Browse the repository at this point in the history
  • Loading branch information
Terry Cain committed Apr 15, 2020
1 parent b86e8ce commit cc8a89a
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 5 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ matrix:
env: EXTRA="boto3"

install:
# unfortunately pipenv check fails so often due to pyup.io/security key we need to ignore it :(
- pip install -U setuptools pip
- if ! [[ -v EXTRA ]]; then
pip install -U pipenv &&
pipenv lock &&
pipenv sync --dev &&
pipenv check &&
(pipenv check || true) &&
pipenv graph;
else
pip install codecov &&
Expand Down
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
Changes
-------

1.0.4 (2020-04-XX)
^^^^^^^^^^^^^^^^^^
* Fixed S3 Presigned Post not being async

1.0.3 (2020-04-09)
^^^^^^^^^^^^^^^^^^
* Fixes typo when using credential process
Expand Down
2 changes: 1 addition & 1 deletion aiobotocore/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .session import get_session, AioSession

__all__ = ['get_session', 'AioSession']
__version__ = '1.0.3'
__version__ = '1.0.4'
3 changes: 2 additions & 1 deletion aiobotocore/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from .client import AioClientCreator, AioBaseClient
from .hooks import AioHierarchicalEmitter
from .parsers import AioResponseParserFactory
from .signers import add_generate_presigned_url
from .signers import add_generate_presigned_url, add_generate_presigned_post
from .credentials import create_credential_resolver, AioCredentials


Expand Down Expand Up @@ -35,6 +35,7 @@ def __init__(self, session_vars=None, event_hooks=None,

# Register our own handlers
self.register('creating-client-class', add_generate_presigned_url)
self.register('creating-client-class.s3', add_generate_presigned_post)

def _register_response_parser_factory(self):
self._components.register_component('response_parser_factory',
Expand Down
94 changes: 93 additions & 1 deletion aiobotocore/signers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import datetime
import botocore
import botocore.auth
from botocore.signers import RequestSigner, UnknownSignatureVersionError, \
UnsupportedSignatureVersionError, create_request_object, prepare_request_dict, \
_should_use_global_endpoint
_should_use_global_endpoint, S3PostPresigner
from botocore.exceptions import UnknownClientMethodError


Expand Down Expand Up @@ -188,3 +189,94 @@ async def generate_presigned_url(self, ClientMethod, Params=None, ExpiresIn=3600
return await request_signer.generate_presigned_url(
request_dict=request_dict, expires_in=expires_in,
operation_name=operation_name)


class AioS3PostPresigner(S3PostPresigner):
async def generate_presigned_post(self, request_dict, fields=None,
conditions=None, expires_in=3600,
region_name=None):
if fields is None:
fields = {}

if conditions is None:
conditions = []

# Create the policy for the post.
policy = {}

# Create an expiration date for the policy
datetime_now = datetime.datetime.utcnow()
expire_date = datetime_now + datetime.timedelta(seconds=expires_in)
policy['expiration'] = expire_date.strftime(botocore.auth.ISO8601)

# Append all of the conditions that the user supplied.
policy['conditions'] = []
for condition in conditions:
policy['conditions'].append(condition)

# Store the policy and the fields in the request for signing
request = create_request_object(request_dict)
request.context['s3-presign-post-fields'] = fields
request.context['s3-presign-post-policy'] = policy

await self._request_signer.sign(
'PutObject', request, region_name, 'presign-post')
# Return the url and the fields for th form to post.
return {'url': request.url, 'fields': fields}


def add_generate_presigned_post(class_attributes, **kwargs):
class_attributes['generate_presigned_post'] = generate_presigned_post


async def generate_presigned_post(self, Bucket, Key, Fields=None, Conditions=None,
ExpiresIn=3600):
bucket = Bucket
key = Key
fields = Fields
conditions = Conditions
expires_in = ExpiresIn

if fields is None:
fields = {}

if conditions is None:
conditions = []

post_presigner = AioS3PostPresigner(self._request_signer)
serializer = self._serializer

# We choose the CreateBucket operation model because its url gets
# serialized to what a presign post requires.
operation_model = self.meta.service_model.operation_model(
'CreateBucket')

# Create a request dict based on the params to serialize.
request_dict = serializer.serialize_to_request(
{'Bucket': bucket}, operation_model)

# Prepare the request dict by including the client's endpoint url.
prepare_request_dict(
request_dict, endpoint_url=self.meta.endpoint_url,
context={
'is_presign_request': True,
'use_global_endpoint': _should_use_global_endpoint(self),
},
)

# Append that the bucket name to the list of conditions.
conditions.append({'bucket': bucket})

# If the key ends with filename, the only constraint that can be
# imposed is if it starts with the specified prefix.
if key.endswith('${filename}'):
conditions.append(["starts-with", '$key', key[:-len('${filename}')]])
else:
conditions.append({'key': key})

# Add the key to the fields.
fields['key'] = key

return await post_presigner.generate_presigned_post(
request_dict=request_dict, fields=fields, conditions=conditions,
expires_in=expires_in)
85 changes: 85 additions & 0 deletions tests/botocore/test_signers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ async def base_signer_setup() -> dict:
}


@pytest.fixture
async def base_signer_setup_s3v4() -> dict:
emitter = mock.AsyncMock()
emitter.emit_until_response.return_value = (None, None)
credentials = aiobotocore.credentials.AioCredentials('key', 'secret')

request_signer = aiobotocore.signers.AioRequestSigner(ServiceId('service_name'),
'region_name', 'signing_name',
's3v4', credentials, emitter)
signer = aiobotocore.signers.AioS3PostPresigner(request_signer)

return {
'credentials': credentials,
'emitter': emitter,
'signer': signer,
'fixed_credentials': await credentials.get_frozen_credentials(),
'request': AWSRequest()
}


@pytest.mark.moto
@pytest.mark.asyncio
async def test_testsigner_get_auth(base_signer_setup: dict):
Expand Down Expand Up @@ -149,3 +169,68 @@ async def test_signers_generate_presigned_urls():

with pytest.raises(UnknownClientMethodError):
await client.generate_presigned_url('lalala')


# From class TestGeneratePresignedPost
@pytest.mark.moto
@pytest.mark.asyncio
async def test_testsigner_generate_presigned_post(base_signer_setup_s3v4: dict):
auth_cls = mock.Mock()
auth_cls.REQUIRES_REGION = True

request_dict = {
'headers': {},
'url': 'https://s3.amazonaws.com/mybucket',
'body': b'',
'url_path': '/',
'method': 'POST',
'context': {}
}

with mock.patch.dict(botocore.auth.AUTH_TYPE_MAPS,
{'s3v4-presign-post': auth_cls}):
signer = base_signer_setup_s3v4['signer']
presigned_url = await signer.generate_presigned_post(
request_dict, conditions=[{'acl': 'public-read'}]
)

auth_cls.assert_called_with(
credentials=base_signer_setup_s3v4['fixed_credentials'],
region_name='region_name', service_name='signing_name'
)
assert presigned_url['url'] == 'https://s3.amazonaws.com/mybucket'


@pytest.mark.moto
@pytest.mark.asyncio
async def test_signers_generate_presigned_post():
with mock.patch('aiobotocore.signers.AioS3PostPresigner.generate_presigned_post') \
as cls_gen_presigned_url_mock:
session = aiobotocore.session.get_session()
async with session.create_client('s3', region_name='us-east-1',
aws_access_key_id='lalala',
aws_secret_access_key='lalala',
aws_session_token='lalala') as client:

await client.generate_presigned_post(
'somebucket',
'someprefix/key'
)

cls_gen_presigned_url_mock.assert_called_once()

cls_gen_presigned_url_mock.reset_mock()

await client.generate_presigned_post(
'somebucket',
'someprefix/${filename}',
{'some': 'fields'},
[{'acl': 'public-read'}]
)

cls_gen_presigned_url_mock.assert_called_once()

cls_gen_presigned_url_mock.reset_mock()

with pytest.raises(UnknownClientMethodError):
await client.generate_presigned_url('lalala')
7 changes: 6 additions & 1 deletion tests/test_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
RestXMLParser, EC2QueryParser, QueryParser, JSONParser, RestJSONParser
from botocore.response import StreamingBody
from botocore.signers import RequestSigner, add_generate_presigned_url, \
generate_presigned_url
generate_presigned_url, S3PostPresigner, add_generate_presigned_post, \
generate_presigned_post
from botocore.hooks import EventAliaser, HierarchicalEmitter
from botocore.utils import ContainerMetadataFetcher, IMDSFetcher, \
InstanceMetadataFetcher
Expand Down Expand Up @@ -242,6 +243,10 @@
RequestSigner.generate_presigned_url: {'2acffdfd926b7b6f6cc4b70b90c0587e7f424888'},
add_generate_presigned_url: {'5820f74ac46b004eb79e00eea1adc467bcf4defe'},
generate_presigned_url: {'9c471f957210c0a71a11f5c73be9fed844ecb5bb'},
S3PostPresigner.generate_presigned_post:
{'b91d50bae4122d7ab540653865ec9294520ac0e1'},
add_generate_presigned_post: {'e30360f2bd893fabf47f5cdb04b0de420ccd414d'},
generate_presigned_post: {'85e9ebe0412cb10716bf84a1533798882f3fc79f'},

# utils.py
ContainerMetadataFetcher.__init__:
Expand Down

0 comments on commit cc8a89a

Please sign in to comment.