firebase-tools icon indicating copy to clipboard operation
firebase-tools copied to clipboard

Add file.getSignedUrl() support in Storage Emulator

Open jsakas opened this issue 3 years ago • 8 comments

Environment info

Running inside firebase functions (also emulated)

"firebase": "8.6.1",
"firebase-admin": "^9.8.0",
"firebase-functions": "^3.14.1",
"firebase-tools": "^9.11.0",

Platform:

macOS Big Sur 11.3.1 (20E241)

Steps to reproduce

Inside any cloud function which processes files and needs to sign a URL:

    const [file] = await bucket.upload(tmpFilePath, { destination: output });
    const signedUrl = await file.getSignedUrl({
      action: 'read',
      expires: addMinutes(new Date(), 60 * 24).toString(),
    });

Expected behavior

I receive a signed URL.

Actual behavior

I receive this error:

[emulators] >  {"verifications":{"app":"MISSING","auth":"MISSING"},"logging.googleapis.com/labels":{"firebase-log-type":"callable-request-verification"},"severity":"INFO","message":"Callable request verification passed"}
[emulators] >  {"severity":"ERROR","message":"Unhandled error Error: Cannot sign data without `client_email`.\n    at GoogleAuth.sign (/Users/jonsakas/Development/GuestHouse/guesthouse-cms/node_modules/google-auth-library/build/src/auth/googleauth.js:631:19)\n    at processTicksAndRejections (internal/process/task_queues.js:97:5)\n    at async sign (/Users/jonsakas/Development/GuestHouse/guesthouse-cms/node_modules/@google-cloud/storage/build/src/signer.js:97:35) {\n  name: 'SigningError'\n}"}

Alternatively, following the documentation that says a project ID is required in the initializeApp I receive a different error:

[emulators] >  {"verifications":{"app":"MISSING","auth":"MISSING"},"logging.googleapis.com/labels":{"firebase-log-type":"callable-request-verification"},"severity":"INFO","message":"Callable request verification passed"}
[emulators] >  {"severity":"ERROR","message":"Unhandled error FirebaseError: Bucket name not specified or invalid. Specify a valid bucket name via the storageBucket option when initializing the app, or specify the bucket name explicitly when calling the getBucket() method.\n    at new FirebaseError (/Users/jonsakas/Development/GuestHouse/guesthouse-cms/node_modules/firebase-admin/lib/utils/error.js:44:28)\n    at Storage.bucket (/Users/jonsakas/Development/GuestHouse/guesthouse-cms/node_modules/firebase-admin/lib/storage/storage.js:104:15)\n    at /Users/jonsakas/Development/GuestHouse/guesthouse-cms/functions/lib/http/downloadAll.js:54:35\n    at async func (/Users/jonsakas/Development/GuestHouse/guesthouse-cms/node_modules/firebase-functions/lib/providers/https.js:336:26) {\n  errorInfo: {\n    code: 'storage/invalid-argument',\n    message: 'Bucket name not specified or invalid. Specify a valid bucket name via the storageBucket option when initializing the app, or specify the bucket name explicitly when calling the getBucket() method.'\n  }\n}"}

jsakas avatar May 24 '21 21:05 jsakas

This issue does not seem to follow the issue template. Make sure you provide all the required information.

google-oss-bot avatar May 24 '21 21:05 google-oss-bot

We currently only have minimal API support for the Cloud API interface, which sadly is completely different and substantially larger than Firebase's, so we're not aiming for 100% compatibility because of this.

Signed URLs are not currently planned to be supported, but we can leave this open for a bit and if you're interested in signed URLs just hit this message with an emoji reaction and we'll keep an eye on it and prioritize accordingly.

abeisgoat avatar May 25 '21 14:05 abeisgoat

I was thinking... When using the emulator, we don't really need an actually signed url, just a url that works with the given request. For example, emulator auth jwt tokens don't have the signature after the payload, but it works.

So, in the storage emulator, perhaps the 'signed url' could simply be the object's url, plus a uuid token (similar to getDownloadUrl), with the difference that, this uuid token is not long-lived and adheres to the permissions and settings provided while calling getSignedUrl()

So, while in production, getSignedUrl() uses the gcloud storage library, but if it detects the emulator - it just uses a stubbed method instead.

Use case

Imagine something like online education - course videos are uploaded and only people who have bought a course are able to view them.

  1. Using custom claims is the best way forward initially, but that limits the max number of courses a user can buy, because the size of the auth token is limited.
  2. Access control lists are also a possible way, but how does that work at scale? I haven't been able to find much info about this, but I do not think we can have objects with huge ACLs including thousands (or millions) of users. In the absence of clear information in that regard, I'm doubtful of going down this route.
  3. We could use node.js and app engine to stream the files to authenticated users. But that ends up destroying a lot of the built-in advantages of using firebase cloud storage. And it also costs more, I suppose - the app engine will have to be alive for the entirety of the stream and have to scale for the many streaming requests from many users.
  4. The best way, is to use getSignedUrl(). First, we prevent all direct reads from firebase storage using storage rules. And in a cloud-function or app-engine, we could check firestore or wherever to see if our user has access to the course, and then make a signed url for the video being requested, if access is allowed, and send that off to the client so that they can stream it on their device. Thus, it's extremely scalable and secure. The only drawback? You will not be able to use storage emulator for this method, having to use production services instead. If there are other parts of your app which are more simple and straight while using storage, then using production cloud storage will also force you to use production auth service (emulator auth tokens won't work). And if you make a mistake, you can't "go back" to a previous state by restarting the emulator.

In my experience, the same hurdle also exists for something like an e-commerce app where I want content to be moderated before being published. And I'm sure there will be others. Using getSignedUrl simplifies a lot of things. It's not like the current emulator doesn't work - it just messes with the dev experience for advanced cases involving storage.

DibyodyutiMondal avatar Aug 15 '21 19:08 DibyodyutiMondal

I filed an internal issue b/197475725 to track this feature request. Please remember to add an emoji reaction to the post by @abeisgoat above if you are interested in this feature and we will prioritize accordingly.

weixifan avatar Aug 23 '21 06:08 weixifan

Any updates on this?

DibyodyutiMondal avatar Nov 26 '21 03:11 DibyodyutiMondal

Waiting...

nabid-pf avatar Dec 10 '21 05:12 nabid-pf

Since it doesn't seem this will get attention soon, you can use the public url when the function is running in the emulator for now. Here's how to do that for a list of files:

      const [images] = await bucket.getFiles({
        prefix: `profile/${profile.id}`,
      })
      const urls = await Promise.all(
        images.map(async (image) =>
          process.env.FUNCTIONS_EMULATOR
            ? image.publicUrl()
            : (
                await image.getSignedUrl({
                  version: 'v4', // Allow to set long expire timestamps
                  action: 'read', // Read Only
                  expires: new Date().getTime() + 24 * 60000, // 24 hours from now
                })
              )[0]
        )
      )

nicolls1 avatar Dec 21 '21 00:12 nicolls1

@jsakas You are missing Authentication info. I think you should not use emulator for this case of use. You should process it directly inside Google Cloud Platform. It is such a waste of time to rely on emulator. I prefer using real-time updates from GCP itself, compared from this emulator.

gOzaru avatar Sep 15 '22 13:09 gOzaru