Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Samples for migrating from Python 2.7 runtime to Python 3.7 #3656

Merged
merged 51 commits into from
Jun 9, 2020
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
cd7aa91
Initial commit for storage sample
engelke Feb 11, 2020
1eaf0c3
Added Python 3.7 version
engelke Feb 11, 2020
4767c57
Starting urlfetch samples
engelke Feb 13, 2020
77355ae
requests samples work in 2.7 and 3.7 (gcloud deploy app app3.yaml for…
engelke Feb 13, 2020
3cdd870
urlfetch async sample Working in Python 3 App Engine
engelke Feb 14, 2020
89d9f10
Working in Python 2 App Engine, too
engelke Feb 14, 2020
9caffad
Added storage test for migration
engelke Feb 18, 2020
4df7416
Use standard environment variable name for bucket
engelke Feb 18, 2020
16f1900
Added tests
engelke Feb 18, 2020
f225af9
WIP on urlfetch authentication
engelke Apr 28, 2020
0157a5e
Added new authenticated replacement for urlfetch
engelke Apr 29, 2020
821b324
Added a test
engelke Apr 29, 2020
9242c48
Incoming app identity verification sample
engelke Apr 29, 2020
e06cfab
Added test
engelke Apr 30, 2020
6b7935c
Added READMEs
engelke May 1, 2020
992c7c2
Update README.md
engelke May 4, 2020
504bc58
Update main.py
engelke May 4, 2020
1fcf1ed
Update main.py
engelke May 4, 2020
fa4b530
Update main.py
engelke May 4, 2020
1672977
Include exception in log message
engelke May 13, 2020
73b1765
Fixed missing newline at EOF
engelke May 13, 2020
561ed96
Removed unused import
engelke May 13, 2020
80572d5
Removed unneeded import
engelke May 13, 2020
05e3664
Minor lint cleanup
engelke May 13, 2020
2d10315
Removed unneeded import
engelke May 13, 2020
3777c1a
Added EOF newline
engelke May 13, 2020
352a436
Lint fix
engelke May 13, 2020
966abce
Merge branch 'master' into msprint
engelke May 13, 2020
800bcfe
Merge branch 'master' into msprint
engelke Jun 1, 2020
8b0f9ed
Removed unused import requests
engelke Jun 1, 2020
a59edbd
Removed unused import
engelke Jun 1, 2020
a89a5df
Tell linter to ignore needed module import order
engelke Jun 1, 2020
c2ef75f
Reordered imports
engelke Jun 1, 2020
dfa8260
Adding requirements-test to samples
engelke Jun 1, 2020
6544799
Linting directive added
engelke Jun 1, 2020
0c73d35
Merge branch 'master' into msprint
engelke Jun 1, 2020
5361c24
Replaced test requirements with Python 2.7 compatible one
engelke Jun 1, 2020
06527de
Merge branch 'msprint' of github.com:GoogleCloudPlatform/python-docs-…
engelke Jun 1, 2020
b72ec73
Another missing requirements-test.txt added
engelke Jun 2, 2020
6fdbe4b
More requirements file updates
engelke Jun 2, 2020
819762b
Typo fixes
engelke Jun 2, 2020
d4b9e51
Migration cleanup for automated tests
engelke Jun 2, 2020
9dba33a
Adjusted tests for Python 2.7
engelke Jun 9, 2020
eb22969
Make lint happier
engelke Jun 9, 2020
d2eb352
Add requests adapter
engelke Jun 9, 2020
2bce712
Needed stub to run in test environment
engelke Jun 9, 2020
c7c3269
Trying to get test to run in this environment
engelke Jun 9, 2020
2eb803a
WIP
engelke Jun 9, 2020
cc8e15d
WIP
engelke Jun 9, 2020
5447691
WIP
engelke Jun 9, 2020
f2ca343
Merge branch 'master' into msprint
engelke Jun 9, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions appengine/standard/migration/incoming/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## App Engine to App Engine Request Sample

This sample application shows how an App Engine for Python 2.7 app can verify
the caller's identity for a request from an App Engine for Python 2.7 or
App Engine for Python 3.7 app.

Requests from an App Engine for Python 2.7 app that use `urlfetch` have
a trustworthy `X-Appengine-Inbound-Appid` header that can be used to verify
the calling app's identity.

Requests from an App Engine for Python 3.7 app that include an Authorization
header with an ID token for the calling app's default service account can
be used to verify those calling apps' identities.

The appengine/standard_python37/migration/urlfetch sample app can be used
to make calls to this app with a valid Authorization header.
25 changes: 25 additions & 0 deletions appengine/standard/migration/incoming/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

runtime: python27
threadsafe: yes
api_version: 1

libraries:
- name: ssl
version: latest

handlers:
- url: .*
script: main.app
20 changes: 20 additions & 0 deletions appengine/standard/migration/incoming/appengine_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# [START vendor]
from google.appengine.ext import vendor

# Add any libraries installed in the "lib" folder.
vendor.add('lib')
# [END vendor]
81 changes: 81 additions & 0 deletions appengine/standard/migration/incoming/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Authenticate requests coming from other App Engine instances.
"""

# [START gae_python_app_identity_incoming]
from google.oauth2 import id_token
from google.auth.transport import requests

import logging
import webapp2


def get_app_id(request):
# Requests from App Engine Standard for Python 2.7 will include a
# trustworthy X-Appengine-Inbound-Appid. Other requests won't have
# that header, as the App Engine runtime will strip it out
incoming_app_id = request.headers.get(
'X-Appengine-Inbound-Appid', None)
if incoming_app_id is not None:
return incoming_app_id

# Other App Engine apps can get an ID token for the App Engine default
# service account, which will identify the application ID. They will
# have to include at token in an Authorization header to be recognized
# by this method.
auth_header = request.headers.get('Authorization', None)
if auth_header is None:
return None

# The auth_header must be in the form Authorization: Bearer token.
bearer, token = auth_header.split()
if bearer.lower() != 'bearer':
return None

try:
info = id_token.verify_oauth2_token(token, requests.Request())
service_account_email = info['email']
incoming_app_id, domain = service_account_email.split('@')
if domain != 'appspot.gserviceaccount.com': # Not App Engine svc acct
return None
else:
return incoming_app_id
Comment on lines +50 to +56
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May also want to verify the audience & issuer here.

The (Python) verify_oauth2_token function verifies the JWT signature, the aud claim, and the exp claim. You must also verify the iss claim and the hd claim (if applicable) by examining the object that verify_oauth2_token returns. If multiple clients access the backend server, also manually verify the aud claim. (ref)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought long and hard about verifying the audience, but eventually decided that I was showing how to migrate from the old X-Appengine-Inbound-Appid approach, which did not perform that check.

The Google verify_oauth2_token verifies tokens issued by Google's OAuth2 server, so I don't think there needs to a separate check of the issuer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM.

The text & samples on their website for other languages vs. Python are a little misleading. I will file a bug.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible that those other language libraries don't do the same issuer
verification automatically.

@engelke Hold on, I thought the text implied that Python doesn't verify issuer automatically.

except Exception as e:
engelke marked this conversation as resolved.
Show resolved Hide resolved
# report or log if desired, as here:
logging.warning('Request has bad OAuth2 id token.')
return None


class MainPage(webapp2.RequestHandler):
allowed_app_ids = [
'other-app-id',
'other-app-id-2'
]

def get(self):
incoming_app_id = get_app_id(self.request)

if incoming_app_id not in self.allowed_app_ids:
self.abort(403)

self.response.write('This is a protected page.')


app = webapp2.WSGIApplication([
('/', MainPage)
], debug=True)
# [END gae_python_app_identity_incoming]
25 changes: 25 additions & 0 deletions appengine/standard/migration/incoming/main_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import webtest

import main


def test_get():
app = webtest.TestApp(main.app)

response = app.get('/')

assert response.status_int == 403
2 changes: 2 additions & 0 deletions appengine/standard/migration/incoming/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
google-auth>=1.14.1
requests>=2.23.0
16 changes: 16 additions & 0 deletions appengine/standard/migration/storage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## App Engine cloudstorage library Replacement

The runtime for App Engine standard for Python 2.7 includes the `cloudstorage`
library, which is used to store and retrieve blobs. The sample in this
directory shows how to do the same operations using Python libraries that
work in either App Engine standard for Python runtime, version 2.7 or 3.7.
The sample code is the same for each environment.

To deploy and run this sample in App Engine standard for Python 2.7:

pip install -t lib -r requirements.txt
gcloud app deploy

To deploy and run this sample in App Engine standard for Python 3.7:

gcloud app deploy app3.yaml
15 changes: 15 additions & 0 deletions appengine/standard/migration/storage/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
runtime: python27
api_version: 1
threadsafe: yes

env_variables:
CLOUD_STORAGE_BUCKET: "REPLACE THIS WITH YOUR EXISTING BUCKET NAME"
BLOB_NAME: "my-demo-blob"

libraries:
- name: ssl
version: latest

handlers:
- url: /.*
script: main.app
5 changes: 5 additions & 0 deletions appengine/standard/migration/storage/app3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
runtime: python37

env_variables:
CLOUD_STORAGE_BUCKET: "REPLACE THIS WITH YOUR EXISTING BUCKET NAME"
BLOB_NAME: "my-demo-blob"
4 changes: 4 additions & 0 deletions appengine/standard/migration/storage/appengine_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from google.appengine.ext import vendor

# Add any libraries installed in the "lib" folder.
vendor.add('lib')
60 changes: 60 additions & 0 deletions appengine/standard/migration/storage/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright 2020 Google, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from flask import Flask, make_response
import os

from google.cloud import storage


app = Flask(__name__)


@app.route('/', methods=['GET'])
def get():
bucket_name = os.environ['CLOUD_STORAGE_BUCKET']
blob_name = os.environ['BLOB_NAME']

client = storage.Client()
bucket = client.bucket(bucket_name)
blob = bucket.blob(blob_name)

response_text = ''

text_to_store = b'abcde\n' + b'f'*1024*4 + b'\n'
blob.upload_from_string(text_to_store)
response_text += 'Stored text in a blob.\n\n'

stored_contents = blob.download_as_string()
if stored_contents == text_to_store:
response_text += 'Downloaded text matches uploaded text.\n\n'
else:
response_text += 'Downloaded text DOES NOT MATCH uploaded text!\n\n'

response_text += 'Blobs in the bucket:\n'
for blob in client.list_blobs(bucket_name):
response_text += ' ' + blob.id + '\n'
response_text += '\n'

bucket.delete_blob(blob_name)
response_text += 'Blob ' + blob_name + ' deleted.\n'

response = make_response(response_text, 200)
response.mimetype = 'text/plain'
return response

engelke marked this conversation as resolved.
Show resolved Hide resolved

if __name__ == '__main__':
# This is used when running locally.
app.run(host='127.0.0.1', port=8080, debug=True)
30 changes: 30 additions & 0 deletions appengine/standard/migration/storage/main_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import main
import os


def test_index():
main.app.testing = True
client = main.app.test_client()

r = client.get('/')
assert r.status_code == 200
assert 'Downloaded text matches uploaded text' in r.data.decode('utf-8')

bucket_name = os.environ['CLOUD_STORAGE_BUCKET']
blob_name = os.environ['BLOB_NAME']
assert ' {}/{}'.format(bucket_name, blob_name) in r.data.decode('utf-8')
assert 'Blob {} deleted.'.format(blob_name) in r.data.decode('utf-8')
2 changes: 2 additions & 0 deletions appengine/standard/migration/storage/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
google-cloud-storage==1.25.0
Flask==1.1.1
23 changes: 23 additions & 0 deletions appengine/standard/migration/urlfetch/async/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## App Engine async urlfetch Replacement

The runtime for App Engine standard for Python 2.7 includes the `urlfetch`
library, which is used to make HTTP(S) requests. There are several related
capabilities provided by that library:

* Straightforward web requests
* Asynchronous web requests
* Platform authenticated web requests to other App Engine apps

The sample in this directory provides a way to make asynchronous web requests
using only generally available Python libraries that work in either App Engine
standard for Python runtime, version 2.7 or 3.7. The sample code is the same
for each environment.

To deploy and run this sample in App Engine standard for Python 2.7:

pip install -t lib -r requirements.txt
gcloud app deploy

To deploy and run this sample in App Engine standard for Python 3.7:

gcloud app deploy app3.yaml
7 changes: 7 additions & 0 deletions appengine/standard/migration/urlfetch/async/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
runtime: python27
api_version: 1
threadsafe: yes

handlers:
- url: .*
script: main.app
1 change: 1 addition & 0 deletions appengine/standard/migration/urlfetch/async/app3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
runtime: python37
25 changes: 25 additions & 0 deletions appengine/standard/migration/urlfetch/async/appengine_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from google.appengine.ext import vendor

# Add any libraries installed in the "lib" folder.
vendor.add('lib')

import requests
import requests_toolbelt.adapters.appengine

# Use the App Engine Requests adapter. This makes sure that Requests uses
# URLFetch.
requests_toolbelt.adapters.appengine.monkeypatch()
Loading