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

// 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?

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

Share Google Doc via Golang SDK

I am using service account JSON to create google Doc via Golang SDK but as this doc is only accessible to Service account I am not able to access it with my Personal Google Account. In the Google Doc SDK documentation I couldn't find any function to share the Doc.
This is my sample Code:
package googledoc
import (
"context"
"fmt"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2/google"
"google.golang.org/api/docs/v1"
"google.golang.org/api/option"
func CreateDoc(title string) error {
ctx := context.Background()
cred, err := GetSecrets("ap-south-1", "GOOGLE_SVC_ACC_JSON")
if err != nil {
return fmt.Errorf("unable to get the SSM %v", err)
}
config, err := google.JWTConfigFromJSON(cred, docs.DocumentsScope)
if err != nil {
log.Fatalf("Unable to parse client secret file to config: %v", err)
}
client := config.Client(ctx)
srv, err := docs.NewService(ctx, option.WithHTTPClient(client))
if err != nil {
log.Fatalf("Unable to retrieve Docs client: %v", err)
}
docObj := docs.Document{
Title: title,
}
doc, err := srv.Documents.Create(&docObj).Do()
if err != nil {
log.Fatalf("Unable to retrieve data from document: %v", err)
return err
}
fmt.Printf("The title of the doc is: %s %s\n", doc.Title, doc.DocumentId)
return nil
}
Any help with this would be really appreciated Thanks.
I believe your goal is as follows.
You want to share the created Google Document by the service account with your Google account.
You want to achieve this using googleapis for golang.
Unfortunately, Google Docs API cannot be used for sharing the Document with users. In this case, Drive API is used. When your script is modified using Drive API, how about the following modification?
Sample script:
Please add this script just after the line of fmt.Printf("The title of the doc is: %s %s\n", doc.Title, doc.DocumentId). By this, the created Google Document is shared with the user.
driveSrv, err := drive.NewService(ctx, option.WithHTTPClient(client)) // Please use your client and service for using Drive API.
if err != nil {
log.Fatal(err)
}
permission := &drive.Permission{
EmailAddress: "###", // Please set the email address you want to share.
Role: "writer",
Type: "user",
}
res, err := driveSrv.Permissions.Create(doc.DocumentId, permission).Do()
if err != nil {
log.Fatal(err)
return err
}
fmt.Println(res)
When this script is added to your script, the created Document is shared with the user as the writer.
Reference:
Permissions: create

Get access token for a google cloud service account in Golang?

I'm using a service account on google cloud. For some reason, I want to get the access token programmatically in golang. I can do gcloud auth application-default print-access-token on the command line.
There is a library by google that seems to allow me to get the token. Here is how I try to use it:
credentials, err := auth.FindDefaultCredentials(ctx)
if err == nil {
glog.Infof("found default credentials. %v", credentials)
token, err2 := credentials.TokenSource.Token()
fmt.Printf("token: %v, err: %v", token, err2)
if err2 != nil {
return nil, err2
}
However, I get an error saying token: <nil>, err: oauth2: cannot fetch token: 400 Bad Request.
I already have GOOGLE_APPLICATION_CREDENTIALS env variable defined and pointing to the json file.
Running your code as-is, returns an err:
Invalid OAuth scope or ID token audience provided
I added the catch-all Cloud Platform writable scope from Google's OAuth scopes:
https://www.googleapis.com/auth/cloud-platform
Doing so, appears to work. See below:
package main
import (
"context"
"log"
"golang.org/x/oauth2"
auth "golang.org/x/oauth2/google"
)
func main() {
var token *oauth2.Token
ctx := context.Background()
scopes := []string{
"https://www.googleapis.com/auth/cloud-platform",
}
credentials, err := auth.FindDefaultCredentials(ctx, scopes...)
if err == nil {
log.Printf("found default credentials. %v", credentials)
token, err = credentials.TokenSource.Token()
log.Printf("token: %v, err: %v", token, err)
if err != nil {
log.Print(err)
}
}
}
I had some challenges using this library recently (to access Cloud Run services which require a JWT audience). On a friend's recommendation, I used google.golang.org/api/idtoken instead. The API is very similar.

Why can I not use a DefaultClient to access Calendar API?

I'd like to use Google's quickstart code to access my Google Calendar. It works when I follow the steps, but changing it to use a DefaultClient results in a 403 error:
$ go run quickstart.go
2021/02/09 14:03:19 Unable to retrieve next ten of the user's events: googleapi: Error 403: Request had insufficient authentication
scopes.
More details:
Reason: insufficientPermissions, Message: Insufficient Permission
exit status 1
This is my current code to retrieve local Default Credentials and use them for generating an httpClient:
client, err := google.DefaultClient(oauth2.NoContext, calendar.CalendarReadonlyScope)
if err != nil {
log.Fatalf("auth broke: %v", err)
}
srv, err := calendar.New(client)
if err != nil {
log.Fatalf("Unable to retrieve Calendar client: %v", err)
}
t := time.Now().Format(time.RFC3339)
events, err := srv.Events.List("primary").ShowDeleted(false).
SingleEvents(true).TimeMin(t).MaxResults(10).OrderBy("startTime").Do()
if err != nil {
log.Fatalf("Unable to retrieve next ten of the user's events: %v", err)
}
fmt.Println("Upcoming events:")
if len(events.Items) == 0 {
fmt.Println("No upcoming events found.")
} else {
for _, item := range events.Items {
date := item.Start.DateTime
if date == "" {
date = item.Start.Date
}
fmt.Printf("%v (%v)\n", item.Summary, date)
}
}
The code does work if I create an OAuth2 credential in my GCP Project and use that, but that's not the approach I'd like to use here.
I understand I may need to use a 3-legged approach to get an OAuth2 token, but I can't find any examples on how to do this with Default Credentials.
Can I not use an approach similar to gcloud CLI's built-in token generation?

Setting up Gmail Push notifications through GCP pub/sub using Go

I'm looking to basically setup my application such that it receives notifications every time a mail hits a Gmail inbox.
I'm following the guide here.
I have a Topic and subscription created.
My credentials are working. I can retrieve my emails using the credentials and a Go script as shown here.
I have enabled permissions on my topic gmail with gmail-api-push#system.gserviceaccount.com as a Pub/Sub Publisher.
I have tested the topic in pub/sub by manually sending a message through the console. The message shows up in the simple subscription I made.
main.go
func main() {
ctx := context.Background()
// Sets your Google Cloud Platform project ID.
projectID := "xxx"
// Creates a client.
// client, err := pubsub.NewClient(ctx, projectID)
_, err := pubsub.NewClient(ctx, projectID)
if err != nil {
log.Fatalf("Failed to create client: %v", err)
}
b, err := ioutil.ReadFile("credentials.json")
if err != nil {
log.Fatalf("Unable to read client secret file: %v", err)
}
// If modifying these scopes, delete your previously saved token.json.
config, err := google.ConfigFromJSON(b, gmail.GmailReadonlyScope)
if err != nil {
log.Fatalf("Unable to parse client secret file to config: %v", err)
}
gmailClient := getClient(config)
svc, err := gmail.New(gmailClient)
if err != nil {
log.Fatalf("Unable to retrieve Gmail client: %v", err)
}
var watchRequest gmail.WatchRequest
watchRequest.TopicName = "projects/xxx/topics/gmail"
svc.Users.Watch("xxx#gmail.com", &watchRequest)
...
The script runs fine although there's no stdout with any confirmation the Watch service is running.
However, with the above setup, I sent a mail from xxx#gmail.com to itself but the mail does not show up in my topic/subscription.
What else must I do to enable Gmail push notification through pub/sub using Go?
Make sure you are calling the Do method on your UsersWatchCall. The svc.Users.Watch returns only the structure, doesn't do the call immediately.
It would look something like this:
res, err := svc.Users.Watch("xxx#gmail.com", &watchRequest).Do()
if err != nil {
// don't forget to check the error
}

Getting 400 invalid_grant on google admin sdk api with golang. Any suggestions?

I am trying to work out a golang script that uses my service account to manage my google domain. I get an error when I try to do a simple user list: 400 invalid_grant. It appears that I am using my service account correctly(?), and my service account is a super admin. I am using credentials in java code; so I know that it is valid. Any thoughts?
package main
import (
"fmt"
"io/ioutil"
"log"
"golang.org/x/net/context"
"golang.org/x/oauth2/google"
directory "google.golang.org/api/admin/directory/v1"
)
func main() {
serviceAccountFile := "/credentials.json"
serviceAccountJSON, err := ioutil.ReadFile(serviceAccountFile)
if err != nil {
log.Fatalf("Could not read service account credentials file, %s => {%s}", serviceAccountFile, err)
}
config, err := google.JWTConfigFromJSON(serviceAccountJSON,
directory.AdminDirectoryUserScope,
directory.AdminDirectoryUserReadonlyScope,
)
// Add the service account.
config.Email = "serviceaccount#domain.com"
srv, err := directory.New(config.Client(context.Background()))
if err != nil {
log.Fatalf("Could not create directory service client => {%s}", err)
}
// The next step fails with:
//2019/03/25 10:38:43 Unable to retrieve users in domain: Get https://www.googleapis.com/admin/directory/v1/users?alt=json&maxResults=10&orderBy=email&prettyPrint=false: oauth2: cannot fetch token: 400 Bad Request
//Response: {
// "error": "invalid_grant",
// "error_description": "Robot is missing a project number."
//}
//exit status 1
usersReport, err := srv.Users.List().MaxResults(10).OrderBy("email").Do()
if err != nil {
log.Fatalf("Unable to retrieve users in domain: %v", err)
}
if len(usersReport.Users) == 0 {
fmt.Print("No users found.\n")
} else {
fmt.Print("Users:\n")
for _, u := range usersReport.Users {
fmt.Printf("%s (%s)\n", u.PrimaryEmail, u.Name.FullName)
}
}
}
I got this working. It seems like it may be a combination of things. DazWilkin, yes I get the 401 unauthorized error when I switch around how I pass in my service account. I made the following changes.
Used Subject instead of Email in the configuration.
Only used the AdminDirectoryUserScope scope, instead of the AdminDirectoryUserReadonlyScope scope (or the combination of both).
Included the Domain in the request. I get a 400 Bad Request without it.
I verified that Directory APIs were on from this link: https://developers.google.com/admin-sdk/directory/v1/quickstart/go . When I clicked on this link, it said that apis were already working. I am using the same json credentials here that I am using in some production scripts written in other languages. Meaning, I thought that this was already in place. So I don't think I needed to do this step, but I will include it in case it is useful for others.
Here is what my script looks like now:
package main
import (
"fmt"
"io/ioutil"
"log"
"golang.org/x/net/context"
"golang.org/x/oauth2/google"
directory "google.golang.org/api/admin/directory/v1"
)
func main() {
serviceAccountFile := "/credentials.json"
serviceAccountJSON, err := ioutil.ReadFile(serviceAccountFile)
if err != nil {
log.Fatalf("Could not read service account credentials file, %s => {%s}", serviceAccountFile, err)
}
// I want to use these options, but these cause a 401 unauthorized error
// config, err := google.JWTConfigFromJSON(serviceAccountJSON,
// directory.AdminDirectoryUserScope,
// directory.AdminDirectoryUserReadonlyScope,
// )
config, err := google.JWTConfigFromJSON(serviceAccountJSON,
directory.AdminDirectoryUserScope,
)
// Add the service account.
//config.Email = "serviceaccount#domain.com" // Don't use Email, use Subject.
config.Subject = "serviceaccount#domain.com"
srv, err := directory.New(config.Client(context.Background()))
if err != nil {
log.Fatalf("Could not create directory service client => {%s}", err)
}
// Get the results.
usersReport, err := srv.Users.List().Domain("domain.com").MaxResults(100).Do()
if err != nil {
log.Fatalf("Unable to retrieve users in domain: %v", err)
}
// Report results.
if len(usersReport.Users) == 0 {
fmt.Print("No users found.\n")
} else {
fmt.Print("Users:\n")
for _, u := range usersReport.Users {
fmt.Printf("%s (%s)\n", u.PrimaryEmail, u.Name.FullName)
}
}
}
Fixed!
Thanks to Sal.
Here's a working Golang example:
https://gist.github.com/DazWilkin/afb0413a25272dc7d855ebec5fcadcb6
NB
Line 24 --config.Subject
Line 31 --You'll need to include the CustomerId (IDPID) using this link
Here's a working Python example:
https://gist.github.com/DazWilkin/dca8c3db8879765632d4c4be8d662074

Resources