4

I've tried researching other threads here on SO and other forums, but still can't overcome this issue. I'm generating a presigned post to S3 and trying to upload a file to it using these headers, but getting a 403: Forbidden.

Permissions The IAM user loaded in with Boto3 has permissions to list, read and write to S3.

IAM permissions

CORS CORS from all origins and all headers are allowed

[
{
    "AllowedHeaders": [
        "*"
    ],
    "AllowedMethods": [
        "GET",
        "HEAD",
        "POST",
        "PUT"
    ],
    "AllowedOrigins": [
        "*"
    ],
    "ExposeHeaders": []
}
]

The code The code is based on Python in Django as well as Javascript. This is the logic:

First the file is retrieved from the HTML, and used to call a function for retrieving the signed URL.

(function () {
    document.getElementById("file-input").onchange = function () {
        let files = document.getElementById("file-input").files;
        let file = files[0];
        Object.defineProperty(file, "name", {
            writeable: true,
            value: `${uuidv4()}.pdf`

        })
        if (!file) {
            return alert("No file selected");
        }
        getSignedRequest(file);
    }
})();

Then a GET request is sent to retrieve the signed URL, using a Django view (described in the next section after this one)

function getSignedRequest(file) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", "/sign_s3?file_name=" + file.name + "&file_type=" + file.type)
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                let response = JSON.parse(xhr.responseText)
                uploadFile(file, response.data, response.url)
            }
            else {
                alert("Could not get signed URL")
            }
        }
    };
    xhr.send()
}

The Django view generating the signed URL

def Sign_s3(request):

    S3_BUCKET = os.environ.get("BUCKET_NAME")

    if (request.method == "GET"):
        file_name = request.GET.get('file_name')
        file_type = request.GET.get('file_type')
        
        s3 = boto3.client('s3', config = boto3.session.Config(signature_version = 's3v4'))
        
        presigned_post = s3.generate_presigned_post(
        Bucket = S3_BUCKET,
        Key = file_name,
        Fields = {"acl": "public-read", "Content-Type": file_type},
        Conditions = [
        {"acl": "public-read"},
        {"Content-Type": file_type}
        ],
        ExpiresIn = 3600
    )


    return JsonResponse({
        "data": presigned_post,
        "url": "https://%s.s3.amazonaws.com/%s" % (S3_BUCKET, file_name)
    })

Finally the file should be uploaded to the bucket (this is where I'm getting the 403 error)

function uploadFile(file, s3Data, url) {
            let xhr = new XMLHttpRequest();
            xhr.open("POST", s3Data.url)

            let postData = new FormData()
            for (key in s3Data.fields) {
                postData.append(key, s3Data.fields[key])
            }

            postData.append("file", file)

            xhr.onreadystatechange = function () {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200 || xhr.status === 204) {
                        document.getElementById("cv-url").value = url
                    }
                    else {
                        alert("Could not upload file")
                    }
                }
            };
            xhr.send(postData)
        }

The network request This is how the network request looks in the browser

Network request 1 Network request 2 Network request 3

Jhnsbrst
  • 318
  • 1
  • 14
  • Make sure the `s3:PutObjectAcl` is also attached to this IAM user. – jellycsc Jun 28 '21 at 13:36
  • Done, it didn't help however... – Jhnsbrst Jun 28 '21 at 14:13
  • Does the bucket have [`Block public access`](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html#access-control-block-public-access-options) turned on? – jellycsc Jun 28 '21 at 14:27
  • Yes, but as I have a signed Post that I'm doing the upload with this shouldn't be a problem, at least to my understanding. – Jhnsbrst Jun 28 '21 at 14:46
  • It will be a problem. You should at least have `BlockPublicAcls` turned off. – jellycsc Jun 28 '21 at 14:50
  • Ok thanks, now it is working. But I though the whole idea with signed URLs was to make the bucket private and only allow signed users to upload files. What does it mean that the first two permissions now are public? I.e. "Block public access to buckets and objects granted through new access control lists (ACLs)" and "Block public access to buckets and objects granted through any access control lists (ACLs)" – Jhnsbrst Jun 28 '21 at 15:01
  • You can remove `"acl": "public-read"` and expose the object through CloudFront. This way you can keep the bucket private. – jellycsc Jun 28 '21 at 15:05

2 Answers2

1

@jellycsc helped me. I had to open up the BlockPublicAcl option for the bucket for it to work.

jellycsc
  • 10,904
  • 2
  • 15
  • 32
Jhnsbrst
  • 318
  • 1
  • 14
0

The URL that you should be using in the upload is supposed to be the one that the presigned response has. Don't just upload whatever url you want.

Update your response to be:

return JsonResponse({
  "data": presigned_post,
  "url": presigned_post.url
})

Specifically the url you are using looks like:

https://BUCKTET_NAME.s3.amazonaws.com/KEY_PATH

When it should look like:

https://s3.REGION.amazonaws.com/BUCKET_NAME

However looking at your code this is what it should be doing, but your screen shot from inspector says otherwise. Why does the url in the network request NOT match the url that was returned by the create_presigned_post request?

Warren Parad
  • 3,910
  • 1
  • 20
  • 29
  • Tried doing it your way with " return JsonResponse({ "data": presigned_post, "url": "https://s3.%s.amazonaws.com/%s" % (os.environ.get("BUCKET_REGION"), S3_BUCKET) })" - but still not working, still getting the 403. The presigned_post object contain an url that looks like the following "https://mybucket.s3.amazonaws.com/" – Jhnsbrst Jun 28 '21 at 14:40