title: Access denied (SA doesn't have storage.objects.create access) when trying to upload using a preSigned url to google cloud storage
body: <p><strong>Background:</strong>
  Have a cloud function deployed to GCP that is responsible for only creating a pre-signed URL for the cloud function invoker to use to upload to Cloud Storage. The front end is the invoker of the cloud function. The front-end then makes the XML request to put the object using the pre-signed URL. I have manually created the service account with the required roles and manually changed the default service account for the cloud function in the gcp console. I am successful in creating the service account, I only encounter my error when trying to use the pre-signed URL.</p>
  <p><strong>Problem:</strong></p>
  <p>Having issues trying to allow a client to upload a file via a pre-signed URL.</p>
  <p>Error received</p>
  <pre><code>&lt;?xml version='1.0' encoding='UTF-8'?&gt;
  &lt;Error&gt;
  &lt;Code&gt;AccessDenied&lt;/Code&gt;
  &lt;Message&gt;Access denied.&lt;/Message&gt;
  &lt;Details&gt;
  urlsigner@&lt;project&gt;.iam.gserviceaccount.com does not have storage.objects.create access to &lt;bucket-name&gt;/&lt;filename&gt;.pdf.
  &lt;/Details&gt;
  &lt;/Error&gt;
  </code></pre>
  <p>Code executes as a gcp cloud function and is used for generating.
  Cloud function is the default service account function was updated to urlSigner</p>
  <pre><code>async createSignedUrl({
      contentType,
      fileName,
      userId,
    }: SignedUrlRequest): Promise&lt;string&gt; {
      const bucket = await this.cloudStorage.bucket(BUCKET_NAME);
      console.log(
        moment.utc().add('minutes', 15).toDate(),
        Date.now() + 15 * 60 * 1000
      );
      const options: any = {
        version: 'v4',
        action: 'write',
        expires: Date.now() + 15 * 60 * 1000,
        contentType,
      };
      const [signedUrl] = await bucket
        .file(`${userId}-${fileName}`)
        .getSignedUrl(options);
  
      return signedUrl;
    }
  </code></pre>
  <p>CORS temp json file</p>
  <pre><code>[
    {
      &quot;maxAgeSeconds&quot;: 3600,
      &quot;method&quot;: [&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;, &quot;OPTIONS&quot;],
      &quot;origin&quot;: [&quot;*&quot;],
      &quot;responseHeader&quot;: [
        &quot;Content-Type&quot;,
        &quot;Authorization&quot;,
        &quot;Content-Length&quot;,
        &quot;User-Agent&quot;,
        &quot;x-goog-resumable&quot;
      ]
    }
  ]
  </code></pre>
  <p>Steps used to create gcp resources</p>
  <p><code>gcloud iam service-accounts create urlsigner --display-name=&quot;GCS URL Signer&quot; --project=&lt;project-id&gt;</code></p>
  <p><code>gcloud iam service-accounts keys create service_account.json --iam-account=urlsigner@&lt;project-id&gt;.iam.gserviceaccount.com</code></p>
  <p><code>gsutil mb gs://&lt;bucket-name&gt;</code></p>
  <p><code>gsutil iam ch serviceAccount:urlsigner@&lt;project-id&gt;.iam.gserviceaccount.com:roles/storage.admin gs://&lt;bucket-name&gt;</code></p>
  <p><code>gsutil cors set cors-json-file.json gs://&lt;bucket-name&gt;</code></p>
  <p>The upload code</p>
  <pre><code>export async function uploadDoc(preSignedUrl: string, file: File) {
    return await new Promise((resolve, reject) =&gt; {
      const xhr = new XMLHttpRequest();
      xhr.open('PUT', preSignedUrl, true);
      xhr.onload = () =&gt; {
        const status = xhr.status;
        if (status === 200) {
          console.log('uploaded');
          resolve('good to go');
        } else {
          console.log('failed to upload');
          reject('not good to go');
        }
      };
  
      xhr.onabort = () =&gt; {
        reject('aborted for idk');
      };
  
      xhr.onerror = () =&gt; {
        reject('Failed for idk');
      };
  
      xhr.setRequestHeader('Content-Type', file.type);
      const formData = new FormData();
      formData.append('verify-doc', file);
      xhr.send(formData);
    });
  }
  </code></pre>
  <p>Lastly, verified that the cloud function has the Service-Token-Creator role necessary for signing the blob that gets sent back to the front-end and that under the bucket permissions the service account is listed with the storage admin role too. So at this point just scratching my head as to why I'm getting the error of access denied for lack of permissions by the service account.</p>
  <p>Thanks in advance for whoever can assist.</p>
  <p>PS.
  I referenced this article as well <a href="https://medium.com/google-cloud/upload-download-files-from-a-browser-with-gcs-signed-urls-and-signed-policy-documents-f66fff8e425" rel="nofollow noreferrer">https://medium.com/google-cloud/upload-download-files-from-a-browser-with-gcs-signed-urls-and-signed-policy-documents-f66fff8e425</a></p>