Google Cloud Function Calling Gmail API Using Service Account Domain-Wide Access

Viewed 71
// Package p contains an HTTP Cloud Function.
package p

import (
    "log"
    "fmt"
    "net/http"
    "context"
    "google.golang.org/api/gmail/v1"
)


func HelloWorld(wx http.ResponseWriter, rx *http.Request) {
    log.Println("start")

    srv, err := gmail.NewService(context.Background())
    if err != nil {
        log.Fatalf("Unable to retrieve Gmail client: %v", err)
    }

    user := "me"
    r, err := srv.Users.Labels.List(user).Do()
    if err != nil {
        log.Fatalf("Unable to retrieve labels: %v", err)
    }
    if len(r.Labels) == 0 {
        fmt.Println("No labels found.")
        return
    }
    fmt.Println("Labels:")
    for _, l := range r.Labels {
        fmt.Printf("- %s\n", l.Name)
    }

    log.Println("end")
}

I'm writing a Google Cloud function in Go that needs to access the Gmail of my admin userid that owns the project, the gmail account, and created the cloud function.

Using the information on the page Choose the best way to use and authenticate service accounts on Google Cloud, I determined I should authenticate using "Attach Service Account" option.

Following the instructions on page Using OAuth 2.0 for Server to Server Applications, I created a new service account and delegated to it domain-wide access to scope https://mail.google.com/ I assigned the new service account as the Runtime Service Account for the Cloud Function.

The gmail.NewService statement seems to execute successfully but the srv.Users.Labels.List(user) statement fails with "Error 400: Precondition check". The log file is below.

2022-07-09T03:34:14.575564Z testgogmail hy9add8bqppl 2022/07/09 03:34:14 start

2022-07-09T03:34:14.785116Z testgogmail hy9add8bqppl 2022/07/09 03:34:14 Unable to retrieve labels: googleapi: Error 400: Precondition check failed., failedPrecondition 

2022-07-09T03:34:14.799538102Z testgogmail hy9add8bqppl Function execution took 553 ms. Finished with status: connection error

So what am I missing? What have I done wrong?

1 Answers

From what I determined, currently it is not possible to impersonate another account using the Gmail API for Go when you are relying on the basic domain-wide access/authentication. The gmail.NewService(context.Background()) statement executes successfully but you are authenticated as the gmail address of the service account. Since that does not actually exist, the subsequent Users.Labels.List fails even if you pass a different/valid email account as the user parameter.

However, it does work if you create an authentication token based on the service account with domain-wide access by using google.JWTConfigFromJSON and then use the WithTokenSource option when creating the Gmail service. Note - the sample code below is based on creating and executing the cloud function from the online UI, not the cloud shell.

    pathWD, err := os.Getwd()
    if err != nil {
        log.Println("error getting working directory:", err)
    }
    log.Println("pathWD: ", pathWD)

    jsonPath := pathWD + "/serverless_function_source_code/"
    serviceAccountFile := "service_account.json"
    serviceAccountFullFile := jsonPath + serviceAccountFile
    os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", serviceAccountFullFile)

    serviceAccountJSON, err := ioutil.ReadFile(serviceAccountFullFile)
    if err != nil {
        log.Fatal(err)
    }

    config, err := google.JWTConfigFromJSON(serviceAccountJSON,
        "https://mail.google.com/", "https://www.googleapis.com/auth/cloud-platform",
    )

    config.Subject = "admin@awarenet.us"

    ctx := context.Background()
    srv, err := gmail.NewService(ctx, option.WithTokenSource(config.TokenSource(ctx)))
    if err != nil {
        log.Fatalf("Unable to retrieve Gmail client: %v", err)
    }
Related