Getting 403 "Anonymous caller does not have storage.objects.get access" when trying to upload to GCS with signed URLs

tomphp picture tomphp · Aug 3, 2019 · Viewed 30k times · Source

I'm trying to upload files directly from the browser to GCS using signed URLs. I'm generating a v4 signed URL from an App Engine Standard PHP application and that seems to be working fine. The problem is when I try to PUT to that URL I get a 403 with the following XML response:

<?xml version='1.0' encoding='UTF-8'?>
<Error>
  <Code>AccessDenied</Code>
  <Message>Access denied.</Message>
  <Details>Anonymous caller does not have storage.objects.create access to <bucket-name>/some-object.txt.</Details>
</Error>

My app engine service account has Service Account Token Creator, which enabled the URL to be created.

I've enabled CORS on the bucket to accept PUT to *, which allowed me to get to where I am now.

I've switched from v2 URLs to v4 as an issue on the Go SDK suggested that was a problem.

I'm generating the signed URL using the PHP Google Cloud Library like so:

$storage = new StorageClient();
$bucket = $storage->bucket('<bucket-name>');
$object = $bucket->object('some-object.txt');

$url = $object->signedUploadUrl(new \DateTime('tomorrow'), ['version' => 'v4']);

I've tried adding the service account to the bucket's permissions and adding Storage Object Admin, Storage Object Creator, etc. but nothing seems to get me past this 403 (apart from opening it up to allUsers).

In this article is says

In addition, within Cloud Storage, you need to grant the following permissions to generate a Signed URL.

  • storage.buckets.get
  • storage.objects.create
  • storage.objects.delete

But I just can't work out which role they need to be added to.

At this point, I think there is one of two possibilities:

  1. The signed URL is not actually working because it should be authenticated as the service account and not anonymous. In this case, what could be causing this?
  2. Signed URLs authenticate as some type of anonymous role but that role does not have the permissions. In which case, how do I add the permissions for that role (allUsers is obviously wrong)?

SOLVED:

There was a number of things wrong with my implementation:

  1. As suggested by Brandon & Charles below, signedUploadUrl is not appropriate for a direct PUT. To get around this, I needed to use beginSignedUploadSession
  2. As suggested by John below, I needed to have Storage Object Creator added on the service account user. This however, is already added on a GAE default service account as it is Project Editor
  3. Service Account Token Creator needs to be explicitly added to the service account as Project Editor doesn't seem to cover it.
  4. I was embedding the URL into Javascript with Twig uses const url='{{ upload_url }}';, however Twig automatically HTML encodes variables so that was breaking the URL, instead, I needed to use {{ upload_url|raw }}. This broken format was the reason that the message included Anonymous caller

Answer

Charles Engelke picture Charles Engelke · Aug 3, 2019

signedUploadUrl creates a URL for the POST HTTP method (see the library source code at https://github.com/googleapis/google-cloud-php/blob/master/Storage/src/StorageObject.php). You are using that signed URL for a PUT request, so the request isn't permitted. The error message does not show this as the problem, but I think that's what it really is.

You can either look into how to upload a file via POST, or create a signed URL for PUT. I've done the latter in Python, but I don't see a way to do it with this library. I'm not a PHP programmer so I might be missing it.

Or you could create your own code to create a signed URL for PUT, starting with the library code as an example. Signed URLs are extremely tricky to get exactly right, and creating your own code will probably be frustrating. It was for me in Python.