Cloud Function with Cloud Function Invoker rights gets 403 status - google-cloud-functions

I have two Cloud Functions: a frontend and an API. I have added a frontend service account to the frontend Cloud Function that has Cloud Function Invoker rights:
I have also explicitly added this service account as a permission to the API Cloud Function, yet I am still getting 403 Not Authorized statuses.
const categories = await this.$axios.$get("/api/v1/categories/", {
params: { hasParent: 0 },
});

You must add the authorization header to make it work! It's not out of the box!
But, the cloud functions doc is bad to explain that. Use the Cloud Run documentation instead (the 2 services use the same underlying infrastructure and the principle are the same, the doc is simply easier to use on Cloud Run!!)

Related

Calling Firebase Hosting API from a Firebase Cloud Function

I have a Firebase (node.js) cloud function that pulls in some data from my app's Firestore database and builds some static content for the web. I'd like that same cloud function to deploy the static content to Firebase hosting via the Firebase Hosting API, creating a static portion of my site with user generated content.
I understand the general flow thanks to the somewhat clear walkthrough, but am stuck on the first step: getting an access token to call the API. Obviously I'm not going to insecurely put my service account key in the cloud function itself, so the example in the walkthrough doesn't apply. And as I understand it, Firebase cloud functions are already associated with a service account, so presumably there's some way to get an access token to call other Google Cloud services from a cloud function.
So how do I get an access token to call the hosting API from a Cloud Function?
There are some red flags that make me think this isn't possible. For example, all of the uses cases in the walkthrough allude to other server environments, as opposed to Google Cloud environments. And yet, this use case is the third bullet in the use case list in the walkthrough.
I've searched extensively here and elsewhere for some guidance, but aren't finding anything. There are some older questions about accessing hosted files from a cloud function that aren't relevant. This promising question from 5 years ago about this exact use case only has dead ends.
You can use the google-auth-library package in Cloud Functions to a get a token as shown below:
import { GoogleAuth } from "google-auth-library";
const token = await new GoogleAuth({
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
}).getAccessToken();
If you use Firebase Admin SDK in the Cloud Functions, then you can get an access token of the default service account as shown below (do ensure the service account has required permissions):
import { initializeApp } from "firebase-admin/app";
const admin = initializeApp();
const token = await admin.options.credential?.getAccessToken();
// ^ Google OAuth2 access token object used to authenticate with Firebase services.

Using Google Cloud Secret Manager with Cloud Functions for Firebase vs Cloud Run

Google Cloud Secret Manager provides a simple and quick way to have access to its secrets with Cloud Functions for Firebase. Basically, you just set some secrets using its intuitive CLI (e.g. firebase functions:secrets:set SECRET_NAME), add a parameter called runWith (e.g. .runWith({ secrets: ["SECRET_NAME"] }) in your source code, and voila. This works even great when testing functions locally!
On the other hand, using SM on Cloud Run requires more steps like setting up services accounts, dependencies, and environment variables (doc). Although using the Google Console to configure secrets, there're still more steps involved.
As an example, here is what a node.js code for Cloud Run would look like (ref):
const {SecretManagerServiceClient} = require('#google-cloud/secret-manager');
const client = new SecretManagerServiceClient();
async function accessSecret(name, version='latest') {
try {
if (!process.env.GOOGLE_PROJECT_ID) {
throw 'Please set the GOOGLE_PROJECT_ID environment variable.';
}
const fullName = `projects/${process.env.GOOGLE_PROJECT_ID}/secrets/` +
`${name}/versions/${version}`;
const [response] = await client.accessSecretVersion({name: fullName});
const payload = response.payload.data.toString();
return payload;
}
catch (ex) {
console.log(ex.toString());
}
}
Is there a better and convenient way to use SM on a code for a Cloud Run project? Also wondering what is the performance impact of the two different approaches on both products.
I’m afraid that there are no other ways to use Secrete manager on cloud run other than following this documentation
Cloud Functions for Firebase has extended behavior of Firebase and integrated Firebase features. The simpler steps you mentioned to integrate secrets with cloud functions for firebase might be the feature added by the firebase.
And you can see that Cloud Functions for gcp somehow follows similar steps as cloud run on gcp.
Cloud run is not supported on Firebase. Hence, it doesn't have extended features like Cloud Functions for firebase.

security doubts about google cloud functions

I've been reading a lot of questions here about security regarding cloud functions (HTTP triggered) and I also read google's official docs but I couldn't find a clear answer for some questions, so I need help.
Please note that this question is about google's cloud functions made from Google cloud console, nothing to do with firebase.
It's possible to make a function "callable" just from my website? I tried to use cors policy but I have it clear that cors have nothing to do with security, so I'm a little bit worried about how I can keep my cloud function "callable" just from my domain.
On the other hand I created a service account on Google Cloud Platform and I'm trying to use it as an invoker. I have set my service account as invoker but how do I use that on my server?
CASE: I'm creating a log for my web, so I created a cloud function that I call every time someone accesses my site: (I'm using Google Tag Manager server-side).
const sendHttpRequest = require("sendHttpRequest");
const postBody = {
testing : true
}
//Calls cloud function
sendHttpRequest(
"<CLOUD FUNTION TRIGGER ADDRESS>",
(statusCode, headers, body) => {
setResponseStatus(200);
setResponseBody("done");
},
{
headers: { "content-type": "application/json; charset=utf-8", "Origin" : "https://example.com" },
method: "POST",
},
postBody
);
}
I would like to know how I can be sure that this cloud function can only be invoked by my server.
Thanks in advance!
Yes, it's possible.
See Authenticating for Invocation.
The second paragraph provides a good synopsis of why this has some complexity.
You're correct in using a Service Account. Service Accounts are used by software. User accounts are used by humans.
It's unclear where your website is running but it will need to generate an identity token (aka JWT) in order to securely invoke the remote Cloud Function.
See the developer testing example in which an identity token is provided by the Cloud SDK (gcloud) using gcloud auth print-identity-token and then used as the Authorization header value with curl.
That's what your website needs to replicate.
The page recommends (correctly) considering using one of Google SDKs to generating tokens programmatically, because the alternative is gnarly and prone to error.
Unless your website is also running on GCP, you can't use the metadata service .

Why is there no IAM role specific to calling a GCP function?

If I look at https://cloud.google.com/functions/docs/reference/iam/roles#standard-roles I see:
roles/cloudfunctions.admin
roles/cloudfunctions.developer
roles/cloudfunctions.viewer
roles/cloudfunctions.invoker
The latter contains only one permission, cloudfunctions.functions.invoke
We are using Google Cloud Workflows to call our cloud function and its currently failing with error:
"error": {
"code": 403,
"message": "Permission 'cloudfunctions.functions.call' denied on resource 'projects/redacted/locations/europe-west2/functions/funcname' (or resource may not exist).",
"status": "PERMISSION_DENIED"
}
I surprised me that given there is a roles/cloudfunctions.invoker role that is no roles/cloudfunctions.caller that includes cloudfunctions.functions.call. roles/cloudfunctions.developer includes that permission but many other things as well Why is there no such role?
And yes, I know I can create a custom role, would just be nice if I didn't have to.
As I understand the Cloud Functions IAM Permissions:
cloudfunctions.functions.call => Call the callFunction API.
cloudfunctions.functions.invoke => Invoke an HTTP function via its public URL.
You mentioned that "We are using Google Cloud Workflows to call our cloud function"... Not sure, but probably you are about this method - projects.locations.functions.call. It is stated on that page: "To be used for testing purposes as very limited traffic is allowed."
I don't know all details of your context and requirements, but can you invoke the cloud function using its URL?
Extra info
You cannot increase the CALL quota. Insufficient quota generally occurs if you mistakenly use this API to invoke your functions in production. Please keep in mind that this API is meant for testing via Cloud Console or ' gcloud functions call CLI, and it cannot handle heavy traffic.
https://cloud.google.com/functions/quotas#rate_limits

Cloud Schedule + Cloud Functions -> Gmail API watch() - WORKING NOW

This is my first post here. I am sorry if it's a repost, but I've been searching for more than one month for the answer to solve my problem in all websites and forums and until now... no answers!
My goal is to make a Gmail pub/sub watch() to make an action whenever I receive a new email.
To do so, according to the developer's website, I need to subscribe to Gmail watch() on a daily basis with the code:
request = {
'labelIds': ['INBOX'],
'topicName': 'projects/myproject/topics/mytopic'
}
gmail.users().watch(userId='me', body=request).execute()
Until now i have this a working scheduled task with a service account, with INVOKER Permissions. This part just works fine.
In my "initial autorization function" i have:
const {google} = require('googleapis');
// Retrieve OAuth2 config
const oauth2Client = new google.auth.OAuth2(
process.env.CLIENT_ID,
process.env.CLIENT_SECRET,
process.env.CALLBACK_URL
);
exports.oauth2init = (req, res) => {
// Define OAuth2 scopes
const scopes = [
'https://www.googleapis.com/auth/gmail.modify'
];
// Generate + redirect to OAuth2 consent form URL
const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes,
//prompt: 'none'// Required in order to receive a refresh token every time
});
return res.redirect(authUrl);
};
My issue now is that the access token is generated via (prompt) the first time and never updates to a new one ( the token expires after 1hour...) it means this code stops working after that period and a "manual" intervention is required. According with the documentation, i need to use "offline" method and on "prompt" i can omit (only requests permissions on the 1st time) or none (never asks), like is said here.
I managed how to make it work! tomorow i will continue with the process.
Should i post here my working code for reference?
Thanks!
I will rephrase the process you illustrated so that there is no ambiguity.
According the documentation you pushed:
You do not suscribed to watch(), you call watch()
watch() is an API call to the Gmail API that will enable automatic events publication on a pub/sub topic you define given conditions you specified. Who are you watching? On what events?
You suscribe to a Pub/Sub topic that is targeted by your previous watch() call
A process (e.g: Google cloud function) suscribes to the topic and will consume messages sent by the Gmail API
The call is to be renewed at least every seven days
Because Google needs to be sure you still need to monitor the targeted inbox, it needs a renewal from you. Another watch() call will act so.
Cloud scheduler will enable this periodic renewal
this service will trigger your renewal script you put in your question. To do so it needs to be authenticated to the platform that host the script. It is easier if your script is hosted in a google service (cloud function, cloud run,...) and the authent type depends on the target URL form. In all cases YOU DO NEED an authent token in your request header. The token is generated from a service account you created with the right permission to call your script (e.g: cloud run invoker). By default the scheduler has the right to generate a token from it
So far so good. Now comes the tricky part and you don't mention it in your question. How is authenticated your gmail api client? You cannot monitor someone inbox, unless this person gave you the permission to i.e you call the API with the right Oauth2 token. Indeed in the video you point they authenticat the user using this principe which is implemented in their code with Express-oauth2-handler.
So you will have a cloud function to init end user authent and watch to his/her inbox. The renewal should do so but problem is user will not be there for accepting the end user consent. Here comes the offline access but it is beyond the scope of your question. Finally a second functions will suscribe to the pubsub topic and consume the message as you need. See their implementation code which populate a spreadsheet.
The documentation you shared in the comments does not say that you can remove the token from the headers of the service account, also the gmail API documentation you also shared says that you only:
need to grant publish privileges to gmail-api-push#system.gserviceaccount.com. You can do this using the Cloud Pub/Sub Developer Console permissions interface following the resource-level access control instructions.
In order to achieve this basically what you will need is a setup of two cloud functions, the first scheduled function is responsible for setting up the watch(), and you can check this documentation for how to deploy a scheduled function, and the second function being triggered by the pubsub of gmail notifications, you can check this documentation for how to build an event triggered function. Both processes are similar.
NOTE: I have never user the Gmail API, so I am not sure if any extra steps are necessary but then again, the documentation implies that setting up the permissions of that service account is enough to make it work.
EDIT:
As per the information you have shared. The issue is likely that you are not properly setting the Service Account to authenticate with the Cloud Function. As per described in the documentation, you have to grant to the Service Account the role Cloud Functions Invoker in IAM.
Let me know if this fixed the issue.