0

I am trying to do the following from within the Compute Engine Instance:

from googleapiclient.discovery import build

service = build('sheets', 'v4')
sheet = service.spreadsheets()
result = sheet.values().get(spreadsheetId=SPREADSHEET_ID, range=RANGE_NAME).execute()

According to this article https://cloud.google.com/docs/authentication/production and also this page in Cloud Console

enter image description here

I don't need to explicitly pass an API key if I am running the app inside compute engine. Yet, I am getting the following error:

  googleapiclient.errors.HttpError: <HttpError 403 when requesting 
  https://sheets.googleapis.com/v4/spreadsheets/[...]?alt=json
  returned "Request had insufficient authentication scopes.".
  Details: "Request had insufficient authentication scopes.">

What am I missing? Do I need to give access to the Sheets API to my instance service account? If so, how do I do it? I can't find anything meaningful in the cloud console. In the Google Example, they are passing the scopes explicitly SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly'] when creating creds, but I don't see how I can pass them here.

Alexei Masterov
  • 382
  • 2
  • 10

1 Answers1

2

I've not tried this but saw your question unanswered...

One issue using Google Apps services is that the data is accessed by named user accounts (e.g. you@company.com) and so you'll either need to use one of the enumerated accounts or make the sheet public (so that anything can access it; you probably don't want to do this).

There's a wrinkle between Cloud and Apps (yes, I know the difference no longer exists in the Google marketing, but...) and, while Cloud services now prefer IAM, the other Google services still use scopes (not IAM). So, munging the service account's IAM permissions won't help.

Your scope is correct.

I think there are (at least) 2 options:

  • Add the service account to the sheet's permissions;
  • Grant the service account delegated auth (link);

All service accounts have an email address.... you should (!?) be able to add your service account's email address to the sheet.

You can find this:

Either gcloud iam service-accounts list --project=${PROJECT}

Or gcloud projects get-iam-policy ${PROJECT}

Then add the email address to the sheet using the sheet's sharing option.

201124 Update w/ example

Pick any Sheets (sheet) and grab its ID from the URL.

Confirm that you're able to access it (using your user credentials) using the (excellent) APIs Explorer specificially for the Sheets v4 API GET method here:

https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get

Create a Google Cloud Platform Project, enable sheets and create a service account and a key.

NOTE Because Sheets does not use IAM, we don't need to assign any permissions to the Service Account:

PROJECT=[[YOUR-PROJECT-ID]]
BILLING=$(gcloud alpha billing accounts list --format="value(name)")

gcloud projects create ${PROJECT}
gcloud beta billing projects link ${PROJECT} --billing-account=${BILLING}

# Enable Sheets API
gcloud services enable sheets.googleapis.com --project=${PROJECT}

# Create Service Account & Key
ROBOT="sheeter"

gcloud iam service-accounts create ${ROBOT} \
--project=${PROJECT}

EMAIL=${ROBOT}@${PROJECT}.iam.gserviceaccount.com

gcloud iam service-accounts keys create  ./${ROBOT}.json \
--iam-account=${EMAIL} \
--project=${PROJECT}

# Use Application Default Credentials
export GOOGLE_APPLICATION_CREDENTIALS=./${ROBOT}.json

# Create Python virtualenv and add Google APIs SDK
python3 -m venv venv
source venv/bin/activate
python3 -m pip install --upgrade google-api-python-client
mkdir python
cd python
touch main.py

Use the following example drawn from Google's example

import google.auth

from pprint import pprint

from googleapiclient import discovery

credentials, project = google.auth.default()

service = discovery.build("sheets", "v4", credentials=credentials)

spreadsheet_id = "[[SPREADSHEET_ID]]"

ranges = []
include_grid_data = False

request = service.spreadsheets().get(spreadsheetId=spreadsheet_id,
                                     ranges=ranges,
                                     includeGridData=include_grid_data)
response = request.execute()

pprint(response)

Replace [[SPREADSHEET_ID]] with the ID of the Sheet you'd like to use

Try running the code, it will 403:

python3 main.py

Add the value of ${EMAIL} to the Sheet's Sharing permissions

enter image description here

Run the code again, it will succeed:

{'properties': {'autoRecalc': 'ON_CHANGE',
                'defaultFormat': {'backgroundColor': {'blue': 1,
                                                      'green': 1,
                                                      'red': 1},
                                  'backgroundColorStyle': {...
             'properties': {'index': 1,
                            'sheetId': [[REDACTED]],
                            'sheetType': 'OBJECT',
                            'title': 'Chart'}}],
 'spreadsheetId': '[[SPREADSHEET_ID]]',
 'spreadsheetUrl': 'https://docs.google.com/spreadsheets/d/[[SPREADSHEET_ID]]/edit'}

201124 Update using Compute Engine

When you create a Compute Engine instance, you must give it the scope to access Sheets:

# Default Scopes
SCOPES="https://www.googleapis.com/auth/devstorage.read_only,\
https://www.googleapis.com/auth/logging.write,\
https://www.googleapis.com/auth/monitoring.write,\
https://www.googleapis.com/auth/servicecontrol,\
https://www.googleapis.com/auth/service.management.readonly,\
https://www.googleapis.com/auth/trace.append"

# Add Sheets
SCOPES="${SCOPES},https://www.googleapis.com/auth/spreadsheets.readonly"

gcloud beta compute instances create sheeter \
--project=${PROJECT} \
--zone=${ZONE} \
--machine-type=f1-micro \
--image-family=debian-10 \
--image-project=debian-cloud \
--scopes=${SCOPES}

You may also run the Instance under a service account of your choosing, e.g.:

gcloud beta compute instances create sheeter \
--project=${PROJECT} \
--zone=${ZONE} \
--machine-type=f1-micro \
--image-family=debian-10 \
--image-project=debian-cloud \
--scopes=${SCOPES} \
--service-account=${EMAIL}

Once the instance is created, repeat the install steps. There's no need to install a key on the instance nor GOOGLE_APPLICATION_CREDENTIALS as these are both provided by Compute Engine.

Check the service account that the Instance runs under:

curl \
--header "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/

Should yield, e.g.:

${PROJECT-NUM}-compute@developer.gserviceaccount.com/
default/

This is the Service Account (in this case Compute Engine's default) under which your code will run. Now, run the code.

python3 main.py
DazWilkin
  • 32,823
  • 5
  • 47
  • 88
  • What is confusing, is the difference between the account that has access to the sheet, and the account that has access to the API. What I am trying to do, is avoid specifying the account that has access the API explicitly, because my program is already running as that account on Compute Engine machine. In other words, I already added the service account to the sheet, or you can assume that the sheet is public. – Alexei Masterov Nov 24 '20 at 16:14
  • Right but, using the first approach you can't avoid adding the (Compute Engine) service account to the sheet because Sheets only approves listed accounts (or domain-wide delegated). What you can do is create a distinct service account for the Compute Engine instance and add that. The code still grabs it as the default but you get to add a specific account to the sheet (not the broader Compute Engine one for the project). – DazWilkin Nov 24 '20 at 16:58
  • Corollary: your code grabs the app default credentials transparently (e.g. the Compute Engine default service account) and presents the account's identity to Sheets. Sheets looks at its authorized list and needs to see that identity listed. Make sense? I guess I should try this for myself ;-) – DazWilkin Nov 24 '20 at 16:59
  • "your code grabs the app default credentials transparently" <-- that's the problem - it doesn't, even though, according to the documentation, it should. – Alexei Masterov Nov 24 '20 at 17:12
  • Ok, I have a working sample for you... I'll update my answer – DazWilkin Nov 24 '20 at 17:58
  • I think you are missing my point. I can get it working with a JSON file present. That's not a problem. If you look at Google documentation that I am referencing in the question, they are stating that I don't need to import the credentials into either appengine or compute engine, because those are already operating as a service account from within the project, and therefore should have the right to access the API. – Alexei Masterov Nov 24 '20 at 18:43
  • Right... In which case you must reference the App Engine or Compute Engine Service Account as appropriate and you don't need to set `GOOGLE_APPLICATION_CREDENTIALS`. But, you must still add the Service Account that's being used to the spreadsheet. If I took my code, ran it on e.g. Compute Engine, I'd need to grab my project's Compute Engine Service Account and add its email to my Sheet – DazWilkin Nov 24 '20 at 19:37
  • Dude! thank you for doing all this research. I really appreciate it. Your answer has detailed instructions for anyone who is starting from scratch. One last question: in your example, is there a way to specify scope from python code, rather than environment variable? – Alexei Masterov Nov 25 '20 at 14:12
  • I spent the last 2 days wondering why I couldn't get a Google Sheets API to work within a GCP VM (even with the sevice account email having access to the sheet and it being the VM active account), just to find out in this answer that the VM scope should include `spreadsheets` access! Thank you very much for this! You saved me from doing some insecure setups on my VM haha! Cheers! – Artur Mello Aug 08 '22 at 22:12
  • @artur-mello I'm pleased to hear that it helped! – DazWilkin Aug 08 '22 at 22:46