simonw / google-drive-to-sqlite

Create a SQLite database containing metadata from Google Drive
https://datasette.io/tools/google-drive-to-sqlite
Apache License 2.0
153 stars 13 forks source link

Support authentication using service account keys #40

Open simonw opened 2 years ago

simonw commented 2 years ago

Service account keys take the form of a JSON file on disk containing a primary key.

It's possible, albeit non-obvious, to make calls to the Google Drive API using these keys.

They might represent a better authentication mechanism for many use-cases. See also:

simonw commented 2 years ago

Creating a service account:

gcloud iam service-accounts create sfms-history-google-drive \                                       
    --display-name="sfms-history-google-drive" \
    --description="Access to the Google Drive for sfms-history" \
    --project sfms-history

It then shows up in:

gcloud iam service-accounts list --project sfms-history

Then create a key like this:

gcloud iam service-accounts keys create /tmp/sfms-history-google-drive.key.json \
    --iam-account="sfms-history-google-drive@sfms-history.iam.gserviceaccount.com" \
    --project sfms-history
simonw commented 2 years ago

Importantly, you can share a Google Drive folder with the email address sfms-history-google-drive@sfms-history.iam.gserviceaccount.com and that service account will now have access to the folder!

simonw commented 2 years ago

I managed to that access programatically using the Google Client libraries:

pip install google-api-python-client

Then:

from google.oauth2 import service_account
from googleapiclient import discovery

scopes = ["https://www.googleapis.com/auth/drive.readonly"]
service_account_file = "/tmp/sfms-history-google-drive.key.json"

credentials = service_account.Credentials.from_service_account_file(service_account_file, scopes=scopes)

drive = discovery.build('drive', 'v3', credentials=credentials)
drive.files().list().execute()

Output starts:

{'kind': 'drive#fileList',
 'nextPageToken': '~!!~AI9FV7St9uKeYX_Iqjg-GvyLDLH37H64zM7NHxq6lBpA_M3WryVvEBzCDBkWxUPRFay8VRv5zvOxSP-OPqLEGQEnOVtwoMtkrtVfO6Y2_irvGVaZR3YQ6anTrdphTLl2HIuATsd07ugWsnZ3pnWGPE45C-mYmrF5qJ5VBHgrmtIXu3gIB2f-mSh_AqAeTRLV1xA8AhMHj6QA9wvx_Fq9PX7w01nZvw5gPAYfOnl_CcdKqg4mcZijZjwJf5fn3kn1lIGcHSF91YORt8PKJQw1RD4mUCcKD9nQZNOPTi67M20lqaEt54D5Akg=',
 'incompleteSearch': False,
 'files': [{'kind': 'drive#file',
   'id': '1EcfXj1Ah3OYX3XqETPGJ1rP7ak53srKf',
   'name': 'SFMS History',
   'mimeType': 'application/vnd.google-apps.folder'},
  {'kind': 'drive#file',
   'id': '1E0NDxHx6yBAwpB7Ay8X3-QydETZQKHwmj5e3PvU72mo',
   'name': 'SFMS Records',
   'mimeType': 'application/vnd.google-apps.spreadsheet'},
  {'kind': 'drive#file',
   'id': '14mqmth3mEDpJ0ww6vQF_Vg8VsJm-U_hB',
   'name': 'Studies in Microscopical Science, Edited by Arthur C Cole FRMS, Volume 1, parts 1 to 52, 1882-1883 (CD from Henry Schott)',

I want to make direct HTTP authenticated calls to the API though, since that's what the code I have written already does.

This works:

import google.auth.transport.requests
from google.auth import _helpers
import httpx

credentials = service_account.Credentials.from_service_account_file(service_account_file, scopes=scopes)
# Weird trick needed to populate credentials.token:
credentials.refresh(google.auth.transport.requests.Request())

response = httpx.get("https://www.googleapis.com/drive/v3/files", headers={
    "authorization": "Bearer {}".format(_helpers.from_bytes(credentials.token))
})
response.json()
simonw commented 2 years ago

Source code of that from_bytes helper function:

def from_bytes(value):
    """Converts bytes to a string value, if necessary.

    Args:
        value (Union[str, bytes]): The value to be converted.

    Returns:
        str: The original value converted to unicode (if bytes) or as passed in
            if it started out as unicode.

    Raises:
        ValueError: If the value could not be converted to unicode.
    """
    result = value.decode("utf-8") if isinstance(value, six.binary_type) else value
    if isinstance(result, six.text_type):
        return result
    else:
        raise ValueError("{0!r} could not be converted to unicode".format(value))