Skip to content

Commit 0108eaf

Browse files
committed
Mod20 fixes and added Mod21
1 parent 32fcf8c commit 0108eaf

File tree

16 files changed

+595
-8
lines changed

16 files changed

+595
-8
lines changed

Diff for: README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ Module | Topic | Video | Codelab | START here | FINISH here
118118
18|Add App Engine `taskqueue` pull tasks| _TBD_ | [link](https://codelabs.developers.google.com/codelabs/cloud-gae-python-migrate-18-gaepull?utm_source=codelabs&utm_medium=et&utm_campaign=CDR_wes_aap-serverless_mgrgaepull_sms_202013&utm_content=-) | Module 1 [code](/mod1-flask) (2.x) | Module 18 [code](/mod18-gaepull) (2.x)
119119
19|Migrate to Cloud Pub/Sub| _TBD_ | [link](https://codelabs.developers.google.com/codelabs/cloud-gae-python-migrate-19-pubsub?utm_source=codelabs&utm_medium=et&utm_campaign=CDR_wes_aap-serverless_mgrpubsub_sms_202016&utm_content=-) | Module 18 [code](/mod18-gaepull) (2.x) | Module 19 [code](/mod19-pubsub) (2.x & 3.x)
120120
20|Add App Engine `users` | _TBD_ | _TBD_ | Module 1 [code](/mod1-flask) (2.x) | Module 20 [code](/mod20-gaeusers) (2.x)
121-
21|Migrate to Cloud Identity Platform | _TBD_ | _TBD_ | Module 20 [code](/mod20-gaeusers) (2.x) | _TBD_
121+
21|Migrate to Cloud Identity Platform | _TBD_ | _TBD_ | Module 20 [code](/mod20-gaeusers) (2.x) | Module 21 [code](/mod21a-idenplat) (2.x) & [code](/mod21b-idenplat) (3.x)
122122

123123

124124
### Table of contents

Diff for: mod20-gaeusers/main.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
from flask import Flask, render_template, request
16-
from google.appengine.api import app_identity, users
16+
from google.appengine.api import users
1717
from google.appengine.ext import ndb
1818

1919
app = Flask(__name__)
@@ -45,8 +45,10 @@ def root():
4545
'who': user.nickname(),
4646
'admin': '(admin)' if users.is_current_user_admin() else '',
4747
'sign': 'Logout',
48-
'link': '/_ah/logout?continue=https://%s/' % \
49-
app_identity.get_default_version_hostname()
48+
'link': '/_ah/logout?continue=%s://%s/' % (
49+
request.environ['wsgi.url_scheme'],
50+
request.environ['HTTP_HOST'],
51+
), # alternative to users.create_logout_url()
5052
} if user else { # not logged in
5153
'who': 'user',
5254
'admin': '',
@@ -55,5 +57,5 @@ def root():
5557
}
5658

5759
# add visits to context and render template
58-
context['visits'] = visits
60+
context['visits'] = visits # display whether logged in or not
5961
return render_template('index.html', **context)

Diff for: mod20-gaeusers/templates/index.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
<head>
44
<title>VisitMe Example</title>
55
</head>
6-
76
<body>
87
<p>
9-
Welcome, {{ who }} <code>{{ admin }}</code> <button id="loginbtn">{{ sign }}</button>
8+
Welcome, {{ who }} <code>{{ admin }}</code>
9+
<button id="logbtn">{{ sign }}</button>
1010
</p><hr>
1111

1212
<h1>VisitMe example</h1>
@@ -18,7 +18,7 @@ <h3>Last 10 visits</h3>
1818
</ul>
1919

2020
<script>
21-
document.getElementById("loginbtn").onclick = () => {
21+
document.getElementById("logbtn").onclick = () => {
2222
window.location.href = '{{ link }}';
2323
};
2424
</script>

Diff for: mod21a-idenplat/.gcloudignore

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# This file specifies files that are *not* uploaded to Google Cloud Platform
2+
# using gcloud. It follows the same syntax as .gitignore, with the addition of
3+
# "#!include" directives (which insert the entries of the given .gitignore-style
4+
# file at that point).
5+
#
6+
# For more information, run:
7+
# $ gcloud topic gcloudignore
8+
#
9+
.gcloudignore
10+
11+
# Source code control files
12+
.git/
13+
.gitignore
14+
.hgignore
15+
.hg/
16+
17+
# README/text files
18+
LICENSE
19+
*.md
20+
21+
# Tests/results (not in .gitignore)
22+
noxfile.py
23+
pylintrc
24+
pylintrc.test
25+
26+
# most of .gitignore (except `lib`)
27+
#
28+
# Python
29+
*.py[cod]
30+
__pycache__/
31+
/setup.cfg
32+
33+
# C extensions
34+
*.so
35+
36+
# Packages
37+
*.egg
38+
*.egg-info
39+
dist
40+
build
41+
eggs
42+
.eggs
43+
parts
44+
bin
45+
var
46+
sdist
47+
develop-eggs
48+
.installed.cfg
49+
lib64
50+
*.tgz
51+
52+
# Installer logs
53+
pip-log.txt
54+
55+
# Tests/results
56+
.nox/
57+
.pytest_cache/
58+
.cache
59+
.pytype
60+
.coverage
61+
coverage.xml
62+
*sponge_log.xml
63+
system_tests/local_test_setup
64+
65+
# Mac
66+
.DS_Store
67+
68+
# IDEs/editors
69+
*.sw[op]
70+
*~
71+
.vscode
72+
.idea
73+
74+
# Built documentation
75+
docs/_build
76+
docs.metadata
77+
78+
# Virtual environment
79+
env/

Diff for: mod21a-idenplat/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Module 21 - Migrate from App Engine `users` to Cloud Identity Platform
2+
3+
This repo folder is the corresponding Python 2 code to the _forthcoming_ Module 21 codelab. The tutorial STARTs with the Python 2 code in the [Module 20 repo folder](/mod20-gaeusers) and leads developers through a migration to Cloud Identity Platform, culminating in the code in this (`mod21a-idenplat`) folder. Also included is a migration from App Engine `ndb` to Google Cloud NDB, mirroring the content covered in [Module 2](http://g.co/codelabs/pae-migrate-cloudndb). There is also a Python 3 version of the app in the [Module 21b](/mod21b-idenplat) folder.

Diff for: mod21a-idenplat/app.yaml

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
runtime: python27
16+
threadsafe: yes
17+
api_version: 1
18+
19+
handlers:
20+
- url: /.*
21+
script: main.app
22+
23+
libraries:
24+
- name: grpcio
25+
version: latest
26+
- name: setuptools
27+
version: latest
28+
- name: ssl
29+
version: latest

Diff for: mod21a-idenplat/appengine_config.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import pkg_resources
16+
from google.appengine.ext import vendor
17+
18+
# Set PATH to your libraries folder.
19+
PATH = 'lib'
20+
# Add libraries installed in the PATH folder.
21+
vendor.add(PATH)
22+
# Add libraries to pkg_resources working set to find the distribution.
23+
pkg_resources.working_set.add_entry(PATH)

Diff for: mod21a-idenplat/main.py

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright 2022 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from flask import Flask, render_template, request
16+
from google.auth import default
17+
from google.cloud import ndb
18+
from googleapiclient import discovery
19+
from firebase_admin import auth, initialize_app
20+
21+
def _get_gae_admins():
22+
'return set of App Engine admins'
23+
# setup constants for calling Cloud IAM Resource Manager
24+
CREDS, PROJ_ID = default( # Application Default Credentials and project ID
25+
['https://www.googleapis.com/auth/cloud-platform'])
26+
IAM = discovery.build('cloudresourcemanager', 'v1', credentials=CREDS)
27+
_TARGETS = frozenset(( # App Engine admin roles
28+
'roles/viewer',
29+
'roles/editor',
30+
'roles/owner',
31+
'roles/appengine.appAdmin',
32+
))
33+
34+
# collate all users who are members of at least one GAE admin role (TARGETS)
35+
admins = set() # set of all App Engine admins
36+
allow_policy = IAM.projects().getIamPolicy(resource=PROJ_ID).execute()
37+
for b in allow_policy['bindings']: # bindings in IAM allow policy
38+
if b['role'] in _TARGETS: # only look at GAE admin roles
39+
admins.update(user.split(':', 1)[1] for user in b['members'])
40+
return admins
41+
42+
@app.route('/is_admin', methods=['POST'])
43+
def is_admin():
44+
'check if user (via their Firebase ID token) is GAE admin (POST) handler'
45+
id_token = request.headers.get('Authorization')
46+
email = auth.verify_id_token(id_token).get('email')
47+
return {'admin': email in _ADMINS}, 200
48+
49+
50+
# initialize Flask, Firebase, Cloud NDB; fetch set of App Engine admins
51+
app = Flask(__name__)
52+
initialize_app()
53+
ds_client = ndb.Client()
54+
_ADMINS = _get_gae_admins()
55+
56+
57+
class Visit(ndb.Model):
58+
'Visit entity registers visitor IP address & timestamp'
59+
visitor = ndb.StringProperty()
60+
timestamp = ndb.DateTimeProperty(auto_now_add=True)
61+
62+
def store_visit(remote_addr, user_agent):
63+
'create new Visit entity in Datastore'
64+
with ds_client.context():
65+
Visit(visitor='{}: {}'.format(remote_addr, user_agent)).put()
66+
67+
def fetch_visits(limit):
68+
'get most recent visits'
69+
with ds_client.context():
70+
return Visit.query().order(-Visit.timestamp).fetch(limit)
71+
72+
73+
@app.route('/')
74+
def root():
75+
'main application (GET) handler'
76+
store_visit(request.remote_addr, request.user_agent)
77+
visits = fetch_visits(10)
78+
return render_template('index.html', visits=visits)

Diff for: mod21a-idenplat/requirements.txt

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
grpcio==1.0.0
2+
protobuf<3.18.0
3+
six>=1.13.0
4+
flask
5+
google-gax<0.13.0
6+
google-api-core==1.31.1
7+
google-api-python-client<=1.11.0
8+
google-auth<2.0dev
9+
google-cloud-datastore==1.15.3
10+
google-cloud-firestore==1.9.0
11+
google-cloud-ndb
12+
google-cloud-pubsub==1.7.0
13+
firebase-admin

Diff for: mod21a-idenplat/templates/index.html

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>VisitMe Example</title>
5+
6+
<script type="module">
7+
// import Firebase module attributes
8+
import {
9+
initializeApp
10+
} from "https://www.gstatic.com/firebasejs/9.10.0/firebase-app.js";
11+
import {
12+
GoogleAuthProvider,
13+
getAuth,
14+
onAuthStateChanged,
15+
signInWithPopup,
16+
signOut
17+
} from "https://www.gstatic.com/firebasejs/9.10.0/firebase-auth.js";
18+
19+
// Firebase config: at least 'apiKey' & 'authDomain' are required; go to
20+
// console.firebase.google.com/project/PROJECT_ID/settings/general/web OR
21+
// console.firebase.google.com/project/_/settings/general/web & pick project
22+
var firebaseConfig = {
23+
apiKey: "YOUR_API_KEY",
24+
authDomain: "YOUR_AUTH_DOMAIN",
25+
};
26+
27+
// initialize Firebase app & auth components
28+
initializeApp(firebaseConfig);
29+
var auth = getAuth();
30+
var provider = new GoogleAuthProvider();
31+
32+
// define login and logout button functions
33+
function login() {
34+
signInWithPopup(auth, provider);
35+
};
36+
37+
function logout() {
38+
signOut(auth);
39+
};
40+
41+
// check if admin & switch to logout button on login; reset everything on logout
42+
onAuthStateChanged(auth, async (user) => {
43+
if (user && user != null) {
44+
var email = user.email;
45+
who.innerHTML = email;
46+
logbtn.onclick = logout;
47+
logbtn.innerHTML = "Logout";
48+
var idToken = await user.getIdToken();
49+
var rsp = await fetch("/is_admin", {
50+
method: "POST",
51+
headers: {Authorization: idToken}
52+
});
53+
var data = await rsp.json();
54+
if (data.admin) {
55+
admin.style.display = "inline";
56+
}
57+
} else {
58+
who.innerHTML = "user";
59+
admin.style.display = "none";
60+
logbtn.onclick = login;
61+
logbtn.innerHTML = "Login";
62+
}
63+
});
64+
</script>
65+
</head>
66+
67+
<body>
68+
<p>
69+
Welcome, <span id="who"></span> <span id="admin"><code>(admin)</code></span>
70+
<button id="logbtn"></button>
71+
</p><hr>
72+
73+
<h1>VisitMe example</h1>
74+
<h3>Last 10 visits</h3>
75+
<ul>
76+
{% for visit in visits %}
77+
<li>{{ visit.timestamp.ctime() }} from {{ visit.visitor }}</li>
78+
{% endfor %}
79+
</ul>
80+
81+
<script>
82+
var who = document.getElementById("who");
83+
var admin = document.getElementById("admin");
84+
var logbtn = document.getElementById("logbtn");
85+
</script>
86+
</body>
87+
</html>

0 commit comments

Comments
 (0)