Accessing Google Drive from a Google App Engine Python app - python

I have an existing Google App Engine Python app with a lot of functionality. I now want to integrate Google Drive into the app. Specifically I want my app to be able to:
Create an empty file in my user's Google Drive where my user can create a Google Doc.
Retrieve that file from Google Drive for further processing in my app.
Send it back to Google Drive periodically so that the user can perform further editing on it as a Google Doc.
I'd be eternally grateful if someone who knows how to do what I'm trying to do can direct me to the SPECIFIC Google webpage(s) that address my SPECIFIC requirement (not a general answer like, "See the DrEdit example"). Thanks in advance!
Update:
Based on the generated sample code in drive-v2-python-appengine per the suggestion in Answer 1, here's my program with a RequestHandler for creating an empty file:
import os
import webapp2
import io
from google.appengine.api import memcache
import httplib2
from apiclient.discovery import build
from apiclient.http import MediaIoBaseUpload
from oauth2client.appengine import oauth2decorator_from_clientsecrets
decorator = oauth2decorator_from_clientsecrets(
os.path.join(os.path.dirname(__file__), 'client_secrets.json'),
scope=[
'https://www.googleapis.com/auth/drive',
])
http = httplib2.Http(memcache)
drive_service = build("drive", "v2", http=http)
class CreateEmptyFile(webapp2.RequestHandler):
#decorator.oauth_required
def get(self):
body = {
'title': 'Sample Document',
'description': 'A sample document',
'mimeType': 'text/plain'
}
media_body = MediaIoBaseUpload(io.BytesIO(""), mimetype='text/plain', resumable=True)
file = drive_service.files().insert(body=body, media_body=media_body).execute()
self.redirect("/synopsis")
Testing is somewhat confusing, because occasionally when I've run this, including the first time, it's brought up the access request page, but most of the time it doesn't. I've used https://accounts.google.com/b/0/IssuedAuthSubTokens?hl=en to revoke access to Drive and Drive no longer shows up on the list, but I guess a time delay of an hour or more exists for carrying out the access revocation. Not sure about that, and haven't seen it documented.
In any case, if I comment-out the call to drive_service.files().insert(), it does not abort, and redirects to my synopsis page. I believe this means the authorization is working correctly, since that makes it like the generated sample code.
However, if I un-comment the insert and use resumable=True for the media body, I get:
ResumableUploadError: Failed to retrieve starting URI.
And if I use resumable=False, I get:
HttpError: <HttpError 401 when requesting https://www.googleapis.com/upload/drive/v2/files?uploadType=multipart&alt=json returned "Login Required">
So I seem to be able to get thru the OAuth 2.0 authorization, but cannot insert a file.

Please try our quickstart app at:
https://developers.google.com/api-client-library/python/start/installation
You can create a quickstart app engine app, which is useful for you to create the initial setup. For specific use-cases, please refer to the drive API reference.

Related

Calling Google App Engine (iAP Enabled) from Google Cloud Function within the same Project

Context:
Node Server in Google App Engine (GAE) that effectively houses a backend for a frontend that is also served by the same app engine instance
Hence why iAP is enabled (for selected web app users only)
Has various endpoints for the frontend to call via reverse-proxy (as I understand it's called)
Google Cloud Function(GCF) within the same project that (funny enough) is actually being called by the node server to initiate the cloud function that then needs to call an endpoint within the GAE node server.
....k wait I might've just found another way to solve the problem but I'll get to that at the end.
I created a VPC Connector for GCF to access a VM instance that I created to talk to external networks. GAE (Flex) is able to do so natively. Not sure if this is relevant but wanted to throw it in the mix.
Short term solution:
Since I need to call the GCF from the GAE node server first, I can just provide it with the relevant data as needed.
Long term solution:
Ideally, the GCF should be called by any other services that might or might not have the data, so it would be ideal to have the GCF call out the GAE endpoint to get the data.
So far:
import urllib
import google.auth.transport.requests
import google.oauth2.id_token
req = urllib.request.Request('https://the-gcp-project-id.appspot.com/api/theEndpoint')
auth_req = google.auth.transport.requests.Request()
id_token = google.oauth2.id_token.fetch_id_token(auth_req, 'https://appengine.googleapis.com')
log.info("Authorization: " + f"Bearer {id_token}")
# req.add_header("Authorization", f"Bearer {id_token}")
# response = urllib.request.urlopen(req)
# # return response.read()
# log.info(response.read())
import requests as reqs
response = reqs.post('https://the-gcp-project-id.appspot.com/theEndpoint', json={'test':'123'}, headers={"Authorization" : f"Bearer {id_token}"})
log.info(response)
This doesn't seem to actually trigger the endpoint though. As far as I know the service account for the cloud function should have the same permissions as the app engine service account.
Can anyone point me in the right direction on this?

What should my URI be for Google Cloud API

I would like to send emails using the Gmail API from a python script I have on my computer.
Here is the code I have so far:
test.py
from google_auth_oauthlib.flow import InstalledAppFlow
flow = InstalledAppFlow.from_client_secrets_file('client_secret.json',scopes=['email'], redirect_uri='http://localhost/authorize/')
flow.run_local_server()
When I execute this, it redirects me to a separate url shows this
My understanding of this problem is that I need to have a redirect uri that matches one on my google cloud platform dashboard for this app. The only thing is I just want to run this locally so I placed the uri http://localhost/authorize instead.
To confirm, I did add http://localhost/authorize to my redirect uri on the Google Cloud Platform.
I referenced this answer I found but could not find my solution:
Correct redirect URI for Google API and OAuth 2.0
I figured out that I needed a Redirect URI because I made the OAuth2 Key for a Web App rather than a Desktop App. In this case, a redirect uri is not needed.

Accessing Office365 Sharepoint REST EndPoints using Python (Office365 Sharepoint REST client for Python)

I am trying to get access to a Sharepoint site from Python and it seems like AADSTS has a conditional access policy that’s preventing me from getting a token.
Has anyone used the Office365 Python Rest Client to successful connect to a Sharepoint Rest Endpoint?
This is the actual error I am seeing
"C:\Python\envs\dev\python.exe c:\Users\xxxxxx.vscode\extensions\ms-python.python-2020.8.105369\pythonFiles\lib\python\debugpy\launcher 52801 -- c:\xxxx\Code\PythonSharepointUpdate\readsharepoint.py " An error occurred while retrieving token from XML response: AADSTS53003: Access has been blocked by Conditional Access policies. The access policy does not allow token issuance. An error occurred while retrieving auth cookies from https://microsoft.sharepoint.com/_vti_bin/idcrl.svc/
I am trying to execute this code from the Sample
from office365.runtime.auth.user_credential import UserCredential
from office365.sharepoint.client_context import ClientContext
from office365.runtime.auth.authentication_context import AuthenticationContext
site_url = 'https://microsoft.sharepoint.com/teams/'
ctx = ClientContext(site_url).with_credentials(UserCredential("user#domain.com", "My Complex Password”))
web = ctx.web
ctx.load(web)
ctx.execute_query()
print("Web title: {0}".format(web.properties['Title']))
Also – is there a better way to authenticate to Sharepoint – I hate to have to type my Password in clear text in code ☹
Has anyone use ClientCredential? Is there a seperate setup on the Sharepoint Site to enable ClientCredential? Does our Sharepoint let us do that?
The trick is to use ClientCredentials and not UserCredentials.
One will have to create a new app on the Sharepoint site with a new CliendID and Client Secret. After creating a new app, the newly created app has to be given permissions on the site. Once the permissions are set, you will have to Trust the app.
The full explanation and the steps are documented here https://github.com/vgrem/Office365-REST-Python-Client/wiki/How-to-connect-to-SharePoint-Online-and-and-SharePoint-2013-2016-2019-on-premises--with-app-principal

Google Drive API 403 error when updating spreadsheet title

We are using Google Drive API in our Google App Engine application.
This weekend we noticed that it has problems with updating spreadsheet title. We are getting the following error:
HttpError: <HttpError 403 when requesting https://www.googleapis.com/drive/v2/files/1_X51WMK0U12rfPKc2x60E_EuyqtQ8koW-NSRZq7Eqdw?quotaUser=5660071165952000&fields=title&alt=json returned "The authenticated user has not granted the app 593604285024 write access to the file 1_X51WMK0U12rfPKc2x60E_EuyqtQ8koW-NSRZq7Eqdw">
Other calls to Google Drive API succeed. We just have the problem with this one. Also this functionality worked properly for a long time. Is it possible that some update on Google side has broken this?
The minimal code to reproduce the issue is:
class TestDriveUpdate(webapp2.RequestHandler):
def get(self):
credentials = StorageByKeyName(Credentials,
'103005000283606027776',
'credentials').get()
spreadsheet_key = '1_X51WMK0U12rfPKc2x60E_EuyqtQ8koW-NSRZq7Eqdw'
quota_user = '5660071165952000'
body = {"title": 'Test'}
fields = "title"
http = httplib2.Http(timeout=60)
credentials.authorize(http)
gdrive = apiclient.discovery.build('drive', 'v2', http=http)
response = gdrive.files().update(
fileId=spreadsheet_key,
body=body,
fields=fields,
quotaUser=quota_user
).execute()
self.response.write("OK")
Based from this documentation, error occurs when the requesting app is not on the ACL for the file and the user never explicitly opened the file with this Drive app. Found this SO question which states that the scope strings must match exactly between your code and the Admin Console, including trailing slashes, etc. Make sure also that Drive Apps are allowed on the domain ("Allow users to install Google Drive apps").

Getting user info with Cloud Endpoints (using other API Endpoints)

I'm trying to setup endpoints api (with google app engine, python), but I'm having some trouble getting user profile info. API is working, I can create entities through API Explorer on my localhost.
My goal is to allow user to register for my app by providing just an email, and authorizing the app to get the reset of the info from their profile. I have this endpoints method:
#User.method(http_method="POST",
auth_level=endpoints.AUTH_LEVEL.REQUIRED,
allowed_client_ids=[
endpoints.API_EXPLORER_CLIENT_ID
],
scopes=[
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/plus.me',
],
user_required=True,
request_fields=('email',),
response_fields=('id',),
name="register",
path="users")
def UserRegister(self, instance):
logging.info(os.getenv( 'HTTP_AUTHORIZATION' ))
# 'Beared __TOKEN__'
logging.info(endpoints.users_id_token._get_token(None))
# '__TOKEN__'
instance.put()
return instance
This works fine, I receive authorization token and user is created in datastore, but I can't figure out how to get the profile info. If I enter the token in OAuth2 API (through API Explorer):
POST https://www.googleapis.com/oauth2/v2/tokeninfo?access_token=__TOKEN__
I get token info with some data I need { "user_id": "__ID__", "verified_email": true, ...}, and if I use user_id in +API:
GET https://www.googleapis.com/plus/v1/people/__ID__
I can get the rest of the data I need (name, image, etc).
What do I need to do to achieve this in my UserRegister() method? I'd prefer to return just entity ID and do the rest of registration asynchronously, but that's another issue, I'll figure it out (; Just need some guidance how to call other endpoints from my code...
EDIT:
I've managed to figure out how to call other APIs (code on Gist), now only have one issue with Plus API:
I did some queries and eventually got anonymous quota error. Then I added key parameter and set it to WEB_CLIENT_ID or SERVICE_ACCOUNT:
WEB_CLIENT_ID is OAuth2 Client ID (type: Web Application) from console.developers.google.com/apis/credentials,
SERVICE_ACCOUNT is default App Engine service account - MY_APP#appspot.gserviceaccount.com...
and now I'm getting following error:
HttpError: <HttpError 400 when requesting https://www.googleapis.com/plus/v1/people/__VALID_USER_ID__?key=__WEB_CLIENT_ID__or__SERVICE_ACCOUNT__&alt=json returned "Bad Request">
When I use +API explorer I get results as expected:
REQUEST:
https://www.googleapis.com/plus/v1/people/__VALID_USER_ID__?key={YOUR_API_KEY}
RESPONSE:
200 OK + json data for user...
Anyone knows why is this happening?
Why am I getting BadRequest response?
Problem with BadRequest was that I didn't send authorization token... I did try to send it as access_token, but seams like +api docs are outdated - it should be oauth_token. When I included this parameter issue was resolved:
build('plus', 'v1').people().get(userId=user_id, key=SERVICE_ACCOUNT, oauth_token=token).execute()
HINT: Use http://localhost:8001/_ah/api/discovery/v1/apis/, and discoveryRestUrl property it has to see real properties of your API - this is where I found the answer.
oauth_token can be obtained like this:
token = os.getenv('HTTP_AUTHORIZATION').split(" ")[1]
# or like in my question:
token = endpoints.users_id_token._get_token(None)
I'd suggest HTTP_AUTHORIZATION variable, because users_id_token docs state that it's a:
Utility library for reading user information from an id_token.
This is an experimental library that can temporarily be used to extract
a user from an id_token. The functionality provided by this library
will be provided elsewhere in the future.
How to call other API Endpoints?
This is also an answer to my first question:
from googleapiclient.discovery import build
service = build('plus', 'v1')
request = service.people().get(userId=user_id, key=SERVICE_ACCOUNT, oauth_token=token)
response = request.execute()
data = dict(self.response.POST)
Code that worked for me is here.
NOTE: WEB_CLIENT_ID obtained from https://console.developers.google.com/apis/credentials (OAuth2 Client ID of type Web Application) will NOT work in this case. I had to use SERVICE_ACCOUNT - I didn't try to generate one through console, default service account I got from App Engine worked fine.
...things are much clearer now that I got this working. Hope it will help someone else (;

Categories