Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to use an AWS signed URL to perform a multipart upload?

I'm writing a client for a service that provides a signed url for uploads. This works just fine for smaller uploads, but fails for larger uploads that would be benefit from using a multipart upload.

The authorization docs suggest that I can use the provided signature and access key id in both the URL or via the Authorization header. I"ve tried using the header approach to start the multipart upload, but I get an access denied. When I use the query string approach, I get a method not allowed (POST in this case).

I'm using boto to generate the URL. For example:

import boto

c = boto.connect_s3()
bucket = c.get_bucket('my-bucket')
key = boto.s3.key.Key(bucket, 'my-big-file.gz')
signed_url = key.generate_url(60 * 60, 'POST')  # expires in an hour

Then when trying to start the multipart upload using the signed URL, I'm doing the following:

import requests

url = signed_url + '&uploads'
resp = requests.post(url)

This returns a method not allowed.

Is this strategy possible? Is there a better way to provide limited credentials to a specific resource in order to allow large multipart uploads?

Update

I've managed to find a slightly more specific error that makes me think this isn't possible. Unfortunately, I get a 403 saying that the signature doesn't match the request.

<?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>
  <StringToSignBytes>.../StringToSignBytes>
  <RequestId>...</RequestId>
  <HostId>...</HostId>
  <SignatureProvided>...</SignatureProvided>
  <StringToSign>POST


  1402941975
  /my-sandbox/test-mp-upload.txt?uploads</StringToSign>
  <AWSAccessKeyId>...</AWSAccessKeyId>
</Error>

This makes me think that I won't be able to use the signed URL because the signature won't match.

UPDATE

I've decided that it is not reasonable to use a signed URL for a multipart upload. While I suspect it is technical possible, it is not practical. The reason being is that a signed URL requires the URL, headers and request method all match exactly in order to work as expected. As a multipart upload needs to initialize the upload, upload each part and finalize (or cancel) the upload, it would be some what painful to generate URLs for each step.

Instead, I found can create a federated token to provide read / write access to a specific key in a bucket. This ends up being more practical and simple because I can immediately use boto as though I had credentials.

like image 756
elarson Avatar asked Jun 15 '14 21:06

elarson


People also ask

How do I use a pre-signed URL?

When you create a presigned URL, you must provide your security credentials and then specify a bucket name, an object key, an HTTP method (PUT for uploading objects), and an expiration date and time. The presigned URLs are valid only for the specified duration.

Does AWS CLI automatically performs multipart upload?

If you're using the AWS Command Line Interface (AWS CLI), then all high-level aws s3 commands automatically perform a multipart upload when the object is large. These high-level commands include aws s3 cp and aws s3 sync.

How do S3 pre-signed URLs work?

You can use presigned URLs to generate a URL that can be used to access your Amazon S3 buckets. When you create a presigned URL, you associate it with a specific action. You can share the URL, and anyone with access to it can perform the action embedded in the URL as if they were the original signing user.


2 Answers

Here is some code to save time for the next person who wants to do this:

import json
from uuid import uuid4

import boto3


def get_upload_credentials_for(bucket, key, username):
    arn = 'arn:aws:s3:::%s/%s' % (bucket, key)
    policy = {"Version": "2012-10-17",
              "Statement": [{
                  "Sid": "Stmt1",
                  "Effect": "Allow",
                  "Action": ["s3:PutObject"],
                  "Resource": [arn],
              }]}
    client = boto3.client('sts')
    response = client.get_federation_token(
        Name=username, Policy=json.dumps(policy))
    return response['Credentials']


def client_from_credentials(service, credentials):
    return boto3.client(
        service,
        aws_access_key_id=credentials['AccessKeyId'],
        aws_secret_access_key=credentials['SecretAccessKey'],
        aws_session_token=credentials['SessionToken'],
    )


def example():
    bucket = 'mybucket'
    filename = '/path/to/file'

    key = uuid4().hex
    print(key)

    prefix = 'tmp_upload_'
    username = prefix + key[:32 - len(prefix)]
    print(username)
    assert len(username) <= 32  # required by the AWS API

    credentials = get_upload_credentials_for(bucket, key, username)
    client = client_from_credentials('s3', credentials)
    client.upload_file(filename, bucket, key)
    client.upload_file(filename, bucket, key + 'bob')  # fails


example()
like image 193
Alex Hall Avatar answered Oct 06 '22 01:10

Alex Hall


To answer my own question, the best option is to use the Security Token Service to generate a set of temporary credentials. A policy description can be provided to limit the credentials to the specific bucket and key.

like image 34
elarson Avatar answered Oct 05 '22 23:10

elarson